mirror of
https://github.com/volatilityfoundation/volatility3.git
synced 2026-08-17 20:35:40 +02:00
Merge pull request #1880 from volatilityfoundation/release/v2.26.2
Release/v2.26.2
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
@@ -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 -
|
||||
|
||||
@@ -43,3 +43,6 @@ ENV/
|
||||
|
||||
# PyTest cache files
|
||||
.pytest_cache/
|
||||
|
||||
# Coverage cache
|
||||
.coverage
|
||||
|
||||
+104
@@ -0,0 +1,104 @@
|
||||
Coding Standards
|
||||
================
|
||||
|
||||
The coding standards for volatility are mostly by our linter and our code formatter.
|
||||
All code submissions will be vetted automatically through tests from both and the submission will not be accepted if either of these fail.
|
||||
|
||||
Code Linter: Ruff
|
||||
Code Formatter: Black
|
||||
|
||||
In addition, there are some coding practices that we employ to prevent specific failure cases and ensure consistency across the codebase. These are documented below along with the rationale for the decision.
|
||||
|
||||
This is heavily based upon https://google.github.io/styleguide/pyguide.html with minor modifications for volatility use.
|
||||
|
||||
Imports
|
||||
-------
|
||||
|
||||
Use import statements for packages and modules only, not for individual types, classes, or functions and ideally not aliased unless the imported name would cause confusion. This is to prevent people from importing something that was itself imported from elsewhere (which can lead to confusion and add in an unnecessary dependency in the import chain).
|
||||
|
||||
* Use `import x` for importing packages and modules.
|
||||
* Use `from x import y` where x is the package prefix and y is the module name with no prefix.
|
||||
* Use `from x import y as z` in any of the following circumstances:
|
||||
* Two modules named `y` are to be imported.
|
||||
* `y` conflicts with a top-level name defined in the current module.
|
||||
* `y` conflicts with a common parameter name that is part of the public API (e.g., `features`).
|
||||
* `y` is an inconveniently long name.
|
||||
* `y` is too generic in the context of your code (e.g., `from storage.file_system import options as fs_options`).
|
||||
|
||||
Exemptions from this rule:
|
||||
|
||||
* Symbols from the following modules are used to support static analysis and type checking:
|
||||
* `typing` module
|
||||
* `collections.abc` module
|
||||
* `typing_extensions` module
|
||||
|
||||
Function calls
|
||||
--------------
|
||||
|
||||
For longer function calls, where line length is no longer an issue, favour using keyword arguments for clarity over unnamed positional arguments.
|
||||
This helps coders learning the code from examples to know what parameters to pass in and avoids ordering mistakes.
|
||||
|
||||
Global Mutable State
|
||||
--------------------
|
||||
|
||||
Avoid mutable global state.
|
||||
|
||||
In those rare cases where using global state is warranted, mutable global entities should be declared at the module level or as a class attribute and made internal by prepending an _ to the name. If necessary, external access to mutable global state must be done through public functions or class methods. See Naming below. Please explain the design reasons why mutable global state is being used in a comment or a doc linked to from a comment.
|
||||
|
||||
Module-level constants are permitted and encouraged. For example: _MAX_HOLY_HANDGRENADE_COUNT = 3 for an internal use constant or SIR_LANCELOTS_FAVORITE_COLOR = "blue" for a public API constant. Constants must be named using all caps with underscores. See Naming below.
|
||||
|
||||
Exceptions
|
||||
----------
|
||||
|
||||
Never use catch-all except: statements, or catch Exception or StandardError, unless you are
|
||||
|
||||
* re-raising the exception, or
|
||||
* creating an isolation point in the program where exceptions are not propagated but are recorded and suppressed instead, such as protecting a thread from crashing by guarding its outermost block.
|
||||
|
||||
Python is very tolerant in this regard and except: will really catch everything including misspelled names, sys.exit() calls, Ctrl+C interrupts, unittest failures and all kinds of other exceptions that you simply don’t want to catch.
|
||||
|
||||
Versioning
|
||||
----------
|
||||
|
||||
Modules that inherit from `VersionableInterface` define a `_version` attribute which states their version. This is a tuple of `(MAJOR, MINOR, PATCH)` numbers, which can then be used for Semantic Versioning (where modifications that change the API in a non-backwards compatible way bump the `MAJOR` version (and set the `MINOR` and `PATCH` to 0) and additive changes increase the `MINOR` version (and set the `PATCH` to 0). Changes that have no effect on the external interface (either input or output form) should have their `PATCH` number incremented. This allows for callers of the interface to determine when changes have happened and whether their code will still work with it. Volatility carries out these checks through the requirements system, where a plugin can define what requirements it has.
|
||||
|
||||
Shared functionality
|
||||
--------------------
|
||||
|
||||
Within a plugin, there may be functions that are useful to other plugins. These are created as `classmethod`s so that the plugin can be depended upon by other plugins in their requirements section, without needing to instantiate a whole copy of the plugin. It is not a staticmethod, because the caller may wish to determine information about the class the method is defined in, and this is not easily accessible for staticmethods.
|
||||
A classmethod usually takes a `context` for its first method (and if it requires one, a configuration string for it second). All other parameters should generally be basic types (such as strings, numbers, etc) so that future work requiring parallelization does not have complex types to have to keep in sync. In particular, the idea was to ensure only one context was used per method (and each object brings its own context with it, meaning the function signature should not include objects to avoid discrepancies).
|
||||
|
||||
Comprehensions
|
||||
--------------
|
||||
|
||||
Comprehensions are allowed, however multiple for clauses or filter expressions are not permitted. Optimize for readability, not conciseness.
|
||||
|
||||
Lambda functions
|
||||
----------------
|
||||
|
||||
Okay for one-liners. Prefer generator expressions over map() or filter() with a lambda.
|
||||
|
||||
Default Arguments
|
||||
-----------------
|
||||
|
||||
Default arguments are fine, but not with mutable types (because they're constructed once at module load time and can lead to confusion/errors.)
|
||||
|
||||
Format strings
|
||||
--------------
|
||||
Generally f-strings are preferred, and where possible a format modifier should be used over a separate method call. As an example, hex output should be `f"0x{offset:x}"` rather than `f"{hex(offset)}"`.
|
||||
F-strings should be used over other formatting methods *except* in cases of logging where the f-string gets calculated/executed whether the log message is displayed or not (where as parameters are not evaluated if not needed).
|
||||
The ruff linter should alert about these situations and exceptions can be maded if needed.
|
||||
|
||||
True/False Evaluations
|
||||
----------------------
|
||||
|
||||
Use the “implicit” false if possible, e.g., if foo: rather than if foo != []:. There are a few caveats that you should keep in mind though:
|
||||
|
||||
* Always use `if foo is None:` (or `is not None`) to check for a `None` value. E.g., when testing whether a variable or argument that defaults to `None` was set to some other value. The other value might be a value that’s false in a boolean context!
|
||||
* Never compare a boolean variable to `False` using `==`. Use `if not x:` instead. If you need to distinguish `False` from `None` then chain the expressions, such as `if not x and x is not None:`.
|
||||
* For sequences (strings, lists, tuples), use the fact that empty sequences are false, so `if seq:` and `if not seq:` are preferable to `if len(seq):` and `if not len(seq):` respectively.
|
||||
|
||||
Logging
|
||||
-------
|
||||
|
||||
We do allow f-string usage in log messages, although technically it should be avoided since it will be evaluated even if the log message is never emitted.
|
||||
@@ -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).
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 system’s network exposure and identify anomalies such as multiple network namespaces, unexpected IP addresses, or active interfaces in promiscuous mode.
|
||||
|
||||
linux.malfind
|
||||
~~~~~~~~~~~~~
|
||||
|
||||
This plugin scans process memory for suspicious executable regions that may indicate code injection or malicious payloads.
|
||||
It is particularly useful for detecting fileless malware, injected shellcode, or unpacked runtime payloads that do not correspond to legitimate binary files on disk.
|
||||
|
||||
.. code-block:: shell-session
|
||||
|
||||
$ python3 vol.py -f memory.vmem linux.malfind
|
||||
|
||||
Volatility 3 Framework 2.26.0
|
||||
Progress: 100.00 Stacking attempts finished
|
||||
PID Process Start End Path Protection Hexdump Disasm
|
||||
|
||||
540 networkd-dispat 0x7f1506482000 0x7f1506483000 Anonymous Mapping rwx
|
||||
00 00 00 00 00 00 00 00 43 00 00 00 00 00 00 00 ........C.......
|
||||
4c 8d 15 f9 ff ff ff ff 25 03 00 00 00 0f 1f 00 L.......%.......
|
||||
...
|
||||
0x7f1506482000: add byte ptr [rax], al
|
||||
0x7f1506482002: add byte ptr [rax], al
|
||||
...
|
||||
0x7f1506482013: stc
|
||||
|
||||
In this output:
|
||||
|
||||
- **PID / Process**: Identifies the target process (in this case, `networkd-dispat`, PID 540)
|
||||
- **Start / End**: The memory address range of the suspicious region
|
||||
- **Path**: Indicates that the region is an anonymous memory mapping (i.e., not backed by a file)
|
||||
- **Protection**: The region is marked `rwx` (read-write-execute), which is uncommon for legitimate memory regions
|
||||
- **Disasm**: Shows the disassembled machine code found in that memory region
|
||||
|
||||
**Key indicators to focus on:**
|
||||
|
||||
- **Anonymous Mapping + rwx**: Memory that is not backed by a file and has execute permissions is often used for injected code
|
||||
- **Disassembly patterns**: Repetitive `add` instructions, `nop`, or unusual instruction sequences can be artifacts of shellcode, packer stubs, or JIT-compiled code
|
||||
- **Process context**: The suspicious memory is found in `networkd-dispat`, a system service — if this service is not expected to have dynamic executable memory regions, it may be compromised
|
||||
|
||||
Use this plugin early in an investigation to flag processes for deeper inspection.
|
||||
|
||||
Further Exploration and Contribution
|
||||
------------------------------------
|
||||
|
||||
This guide has introduced several key Linux plugins available in Volatility 3 for memory forensics.
|
||||
However, many more plugins are available, covering topics such as kernel modules, page cache analysis, tracing frameworks, and malware detection.
|
||||
|
||||
If you identify gaps in plugin functionality or wish to extend support for a specific analysis use case, you are encouraged to contribute new plugins or enhancements.
|
||||
Your insights can help shape the future of Linux memory forensics.
|
||||
|
||||
|
||||
@@ -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
@@ -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
@@ -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.
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"""
|
||||
|
||||
@@ -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,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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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(),
|
||||
)
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
@@ -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),
|
||||
)
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
),
|
||||
)
|
||||
|
||||
@@ -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"]]
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user