From 3fc8b9cd31aaaf9a01b5fe12b4745e4884448c7b Mon Sep 17 00:00:00 2001 From: Mike Auty Date: Mon, 27 Jan 2025 00:04:28 +0000 Subject: [PATCH 001/172] Documentation: Initial version of the coding style guide. --- CODING_STYLE.md | 62 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 62 insertions(+) create mode 100644 CODING_STYLE.md diff --git a/CODING_STYLE.md b/CODING_STYLE.md new file mode 100644 index 000000000..f67afefb4 --- /dev/null +++ b/CODING_STYLE.md @@ -0,0 +1,62 @@ +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 additio, 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. + +Imports +------- + +Import should be of a module (not a class or a method), and ideally just one module except where naming would cause confusion. +This is to prevent people importing an imported method (which can lead to confusion and add in an unnecessary dependency in the import chain). + +Good example: + +``` +from module import submodule +from module.submodule import submodule as subsubmodule + +class NewClass(submodule.Class): + def method(self): + submodule.Class.classmethod() +``` + +Bad example: + +``` +from module import method +from module.submodule import Class +``` + +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 paralellization 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). From ffd0363c4b987b3e5e981d8adf7e5ea88c4b97fa Mon Sep 17 00:00:00 2001 From: Mike Auty Date: Mon, 27 Jan 2025 00:28:51 +0000 Subject: [PATCH 002/172] Bulk out with text from the google python style guide where this aligns --- CODING_STYLE.md | 102 +++++++++++++++++++++++++++++++----------------- 1 file changed, 66 insertions(+), 36 deletions(-) diff --git a/CODING_STYLE.md b/CODING_STYLE.md index f67afefb4..3695301eb 100644 --- a/CODING_STYLE.md +++ b/CODING_STYLE.md @@ -2,61 +2,91 @@ 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. +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 additio, 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. +In additiom, 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 ------- -Import should be of a module (not a class or a method), and ideally just one module except where naming would cause confusion. -This is to prevent people importing an imported method (which can lead to confusion and add in an unnecessary dependency in the import chain). +Use import statements for packages and modules only, not for individual types, classes, or functions and ideally not aliased naming would cause confusion. This is to prevent people importing an imported method (which can lead to confusion and add in an unnecessary dependency in the import chain). -Good example: +* 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`). -``` -from module import submodule -from module.submodule import submodule as subsubmodule +Exemptions from this rule: -class NewClass(submodule.Class): - def method(self): - submodule.Class.classmethod() -``` + * Symbols from the following modules are used to support static analysis and type checking: + * `typing` module + * `collections.abc` module + * `typing_extensions` module -Bad example: +Global Mutable State +-------------------- -``` -from module import method -from module.submodule import Class -``` +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. +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. +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 paralellization 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). -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 paralellization 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 consructed once at module load time and can lead to confusion/errors.) + +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. From 56a64150ab6850c7f4b481355dbabc43d80c10e4 Mon Sep 17 00:00:00 2001 From: Mike Auty Date: Thu, 27 Feb 2025 10:27:13 +0000 Subject: [PATCH 003/172] Core: Update coding requirement on function calls --- CODING_STYLE.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CODING_STYLE.md b/CODING_STYLE.md index 3695301eb..2f18959c8 100644 --- a/CODING_STYLE.md +++ b/CODING_STYLE.md @@ -32,6 +32,12 @@ Exemptions from this rule: * `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 -------------------- From d753e8ecb6c070c9834cdf4e11ecfb6ac807bd29 Mon Sep 17 00:00:00 2001 From: ikelos Date: Tue, 25 Mar 2025 17:25:55 +0000 Subject: [PATCH 004/172] Update CODING_STYLE.md Thanks, my fingers don't work as well as they used to, so I appreciate you picking up typos like this! Co-authored-by: Donghyun Kim --- CODING_STYLE.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CODING_STYLE.md b/CODING_STYLE.md index 2f18959c8..8301eebc2 100644 --- a/CODING_STYLE.md +++ b/CODING_STYLE.md @@ -7,7 +7,7 @@ All code submissions will be vetted automatically through tests from both and th Code Linter: Ruff Code Formatter: Black -In additiom, 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. +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. From 5c2d646829b2806a3874ee3b5538c7c33127dd98 Mon Sep 17 00:00:00 2001 From: ikelos Date: Tue, 25 Mar 2025 17:27:06 +0000 Subject: [PATCH 005/172] Update CODING_STYLE.md Co-authored-by: Donghyun Kim --- CODING_STYLE.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CODING_STYLE.md b/CODING_STYLE.md index 8301eebc2..bf12288d3 100644 --- a/CODING_STYLE.md +++ b/CODING_STYLE.md @@ -81,7 +81,7 @@ Okay for one-liners. Prefer generator expressions over map() or filter() with a Default Arguments ----------------- -Default arguments are fine, but not with mutable types (because they're consructed once at module load time and can lead to confusion/errors.) +Default arguments are fine, but not with mutable types (because they're constructed once at module load time and can lead to confusion/errors.) True/False Evaluations ---------------------- From f0b6b0405fa98636579e25e27b23880215beea97 Mon Sep 17 00:00:00 2001 From: ikelos Date: Tue, 25 Mar 2025 17:27:31 +0000 Subject: [PATCH 006/172] Update CODING_STYLE.md Hehehe, good catch, thanks! 5:D Co-authored-by: Donghyun Kim --- CODING_STYLE.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CODING_STYLE.md b/CODING_STYLE.md index bf12288d3..700167258 100644 --- a/CODING_STYLE.md +++ b/CODING_STYLE.md @@ -66,7 +66,7 @@ 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 paralellization 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). +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 -------------- From 3b21b350f1e470c064396055e6e0c0c17c2940a6 Mon Sep 17 00:00:00 2001 From: Mike Auty Date: Thu, 3 Apr 2025 17:04:18 +0100 Subject: [PATCH 007/172] Documentation: Add in a small section on the coding style about using f-string modifiers over separate calls --- CODING_STYLE.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CODING_STYLE.md b/CODING_STYLE.md index 700167258..520450fe8 100644 --- a/CODING_STYLE.md +++ b/CODING_STYLE.md @@ -83,6 +83,12 @@ 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 ---------------------- From 338c76c2ca8deb0a6f085e0effe7600f0f836ba9 Mon Sep 17 00:00:00 2001 From: Mike Auty Date: Thu, 3 Apr 2025 17:08:13 +0100 Subject: [PATCH 008/172] Documentation: Add in @dgmcdonna suggestions/corrections --- CODING_STYLE.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CODING_STYLE.md b/CODING_STYLE.md index 520450fe8..a4e248ffe 100644 --- a/CODING_STYLE.md +++ b/CODING_STYLE.md @@ -14,10 +14,10 @@ This is heavily based upon https://google.github.io/styleguide/pyguide.html with Imports ------- -Use import statements for packages and modules only, not for individual types, classes, or functions and ideally not aliased naming would cause confusion. This is to prevent people importing an imported method (which can lead to confusion and add in an unnecessary dependency in the import chain). +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` 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. From e9a4ea1a9c4f9fe18571ec9734406f60de2c0b99 Mon Sep 17 00:00:00 2001 From: David McDonald Date: Mon, 7 Apr 2025 15:39:23 -0500 Subject: [PATCH 009/172] Fix traceback in volshell's `dt()` A `SymbolError` can occur when a type contains a pointer to an opaque type. For example, `_EPROCESS` can have a member that points to an `_EPROCESS_QUOTA_BLOCK`, but there is no definition for that type, so its size and readability can't be determined. This wraps the block in a try/except, and reports that the type has an unknown size in the suffix if a `SymbolError` occurs. --- volatility3/cli/volshell/generic.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/volatility3/cli/volshell/generic.py b/volatility3/cli/volshell/generic.py index 32a5bd933..7c85eec5f 100644 --- a/volatility3/cli/volshell/generic.py +++ b/volatility3/cli/volshell/generic.py @@ -621,12 +621,15 @@ class Volshell(interfaces.plugins.PluginInterface): if isinstance(value, objects.Pointer): # show pointers in hex to match output for struct addrs # highlight null or unreadable pointers - if value == 0: - suffix = " (null pointer)" - elif not value.is_readable(): - suffix = " (unreadable pointer)" - else: - suffix = "" + try: + if value == 0: + suffix = " (null pointer)" + elif not value.is_readable(): + suffix = " (unreadable pointer)" + else: + suffix = "" + except exceptions.SymbolError as exc: + suffix = f" (pointer to {exc.symbol_name} - unknown size)" return f"{hex(value)}{suffix}" elif isinstance(value, objects.PrimitiveObject): return repr(value) From 195f4154e97c5134ee390f0cbe81e0eef156225c Mon Sep 17 00:00:00 2001 From: David McDonald Date: Mon, 7 Apr 2025 16:18:22 -0500 Subject: [PATCH 010/172] Shorten suffix --- volatility3/cli/volshell/generic.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/volatility3/cli/volshell/generic.py b/volatility3/cli/volshell/generic.py index 7c85eec5f..2ea722a37 100644 --- a/volatility3/cli/volshell/generic.py +++ b/volatility3/cli/volshell/generic.py @@ -629,7 +629,7 @@ class Volshell(interfaces.plugins.PluginInterface): else: suffix = "" except exceptions.SymbolError as exc: - suffix = f" (pointer to {exc.symbol_name} - unknown size)" + suffix = f" (unknown sized {exc.symbol_name})" return f"{hex(value)}{suffix}" elif isinstance(value, objects.PrimitiveObject): return repr(value) From 4045b6cc8e99a9610d1b39370b3fb2a80f666741 Mon Sep 17 00:00:00 2001 From: David McDonald Date: Tue, 8 Apr 2025 09:30:31 -0500 Subject: [PATCH 011/172] InodePages: Add output column and render when dumped Previously, the InodePages plugin wasn't rendering treegrid columns when the `--dump` flag was passed. This fixes that, and adds an additional `Output File` column that displays the name of the file containing the dumped data. --- volatility3/framework/plugins/linux/pagecache.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/volatility3/framework/plugins/linux/pagecache.py b/volatility3/framework/plugins/linux/pagecache.py index 0bb3b9263..60b8b066b 100644 --- a/volatility3/framework/plugins/linux/pagecache.py +++ b/volatility3/framework/plugins/linux/pagecache.py @@ -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 @@ -541,6 +541,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 +570,7 @@ class InodePages(plugins.PluginInterface): page_index, dump_safe, page_flags, + filename, ) yield 0, fields @@ -610,6 +612,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 +621,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 +631,7 @@ class InodePages(plugins.PluginInterface): ("Index", int), ("DumpSafe", bool), ("Flags", str), + ("Output File", str), ] return renderers.TreeGrid( From 5cc8e360731b27563290f4e40da73b0d00e02364 Mon Sep 17 00:00:00 2001 From: Mike Auty Date: Tue, 8 Apr 2025 16:04:25 +0100 Subject: [PATCH 012/172] Documentation: Improve the readme to make quickstart the first thing people see --- README.md | 58 +++++++++++++++++++++++++++++-------------------------- 1 file changed, 31 insertions(+), 27 deletions(-) diff --git a/README.md b/README.md index 4f5a0a37e..3c8698c12 100644 --- a/README.md +++ b/README.md @@ -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. + ``` + +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 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 -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 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 -h` for more information on a particular command. - ## Symbol Tables Symbol table packs for the various operating systems are available for download at: - - - + + + The hashes to verify whether any of the symbol pack files have downloaded successfully or have changed can be found at: - - - + + + Symbol tables zip files must be placed, as named, into the `volatility3/symbols` directory (or just the symbols directory next to the executable file). From 54f64fcab897672ba6a5a66f8ae1449b38fa1804 Mon Sep 17 00:00:00 2001 From: Abyss Watcher Date: Tue, 8 Apr 2025 19:48:15 +0200 Subject: [PATCH 013/172] comparison harness: module alignment --- volatility3/framework/symbols/linux/utilities/modules.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/volatility3/framework/symbols/linux/utilities/modules.py b/volatility3/framework/symbols/linux/utilities/modules.py index 62ebeef03..16ce93441 100644 --- a/volatility3/framework/symbols/linux/utilities/modules.py +++ b/volatility3/framework/symbols/linux/utilities/modules.py @@ -474,10 +474,9 @@ class Modules(interfaces.configuration.VersionableInterface): Returns: The struct module alignment """ - # FIXME: When dwarf2json/ISF supports type alignments. Read it directly from the type metadata - # Additionally, while 'context' and 'vmlinux_module_name' are currently unused, they will be - # essential for retrieving type metadata in the future. - return 64 + # Not L1 cache aligned, but compiler should naturally + # align to the referenced type as a minimum. + return context.modules[vmlinux_module_name].get_type("pointer").size @classmethod def list_modules( From f9bd9fb94e1599a11df56cd8bfbf4f115b176fcf Mon Sep 17 00:00:00 2001 From: Abyss Watcher Date: Tue, 8 Apr 2025 20:08:15 +0200 Subject: [PATCH 014/172] comment wasn't really applicable here --- volatility3/framework/symbols/linux/utilities/modules.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/volatility3/framework/symbols/linux/utilities/modules.py b/volatility3/framework/symbols/linux/utilities/modules.py index 16ce93441..1c675a283 100644 --- a/volatility3/framework/symbols/linux/utilities/modules.py +++ b/volatility3/framework/symbols/linux/utilities/modules.py @@ -474,8 +474,6 @@ class Modules(interfaces.configuration.VersionableInterface): Returns: The struct module alignment """ - # Not L1 cache aligned, but compiler should naturally - # align to the referenced type as a minimum. return context.modules[vmlinux_module_name].get_type("pointer").size @classmethod From ac8a1101ce317a8724d8c68f45b3b090493ce981 Mon Sep 17 00:00:00 2001 From: ikelos Date: Tue, 8 Apr 2025 20:18:10 +0100 Subject: [PATCH 015/172] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 3c8698c12..cf735fc8d 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,7 @@ more details. 1. Install the required dependencies: ```shell - pip install --user -e. + pip install --user -e ".[full]" ``` 2. See available options: From 64ecd65d2c8ccb5e9284182104266e97d8fabce4 Mon Sep 17 00:00:00 2001 From: David McDonald Date: Tue, 8 Apr 2025 14:37:29 -0500 Subject: [PATCH 016/172] Windows: Improve type-hints in thrdscan, extensions This improves type-hinting in the `ThrdScan` class and in the `ETHREAD` extension class through narrowing the return type of some methods from `interfaces.objects.ObjectInterface` to their actual return type, `extensions.ETHREAD`. Also creates a `NamedTuple` for holding thread info, which cleans up the type signature and makes the returned value easier for consumers to use. --- .../framework/plugins/windows/thrdscan.py | 77 ++++++++----------- .../symbols/windows/extensions/__init__.py | 10 ++- 2 files changed, 41 insertions(+), 46 deletions(-) diff --git a/volatility3/framework/plugins/windows/thrdscan.py b/volatility3/framework/plugins/windows/thrdscan.py index 0ac3d0c33..49103cc7e 100644 --- a/volatility3/framework/plugins/windows/thrdscan.py +++ b/volatility3/framework/plugins/windows/thrdscan.py @@ -1,15 +1,16 @@ ## ## plugin for testing addition of threads scan support to poolscanner.py ## -import logging import datetime -from typing import Callable, Iterable, Tuple, Optional, Dict +import logging +from typing import Callable, Dict, NamedTuple, Optional, Union, Tuple, Iterator -from volatility3.framework import renderers, interfaces, exceptions +from volatility3.framework import exceptions, interfaces, objects, renderers from volatility3.framework.configuration import requirements from volatility3.framework.renderers import format_hints -from volatility3.plugins.windows import poolscanner, pe_symbols +from volatility3.framework.symbols.windows import extensions as win_extensions from volatility3.plugins import timeliner +from volatility3.plugins.windows import pe_symbols, poolscanner vollog = logging.getLogger(__name__) @@ -21,6 +22,17 @@ class ThrdScan(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface) _required_framework_version = (2, 6, 0) _version = (2, 0, 0) + class ThreadInfo(NamedTuple): + offset: int + pid: objects.Pointer + tid: objects.Pointer + start_addr: objects.Pointer + start_path: Optional[str] + win32_start_addr: objects.Pointer + win32_start_path: Optional[str] + create_time: Union[datetime.datetime, interfaces.renderers.BaseAbsentValue] + exit_time: Union[datetime.datetime, interfaces.renderers.BaseAbsentValue] + def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.implementation = self.scan_threads @@ -51,7 +63,7 @@ class ThrdScan(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface) cls, context: interfaces.context.ContextInterface, module_name: str, - ) -> Iterable[interfaces.objects.ObjectInterface]: + ) -> Iterator[win_extensions.ETHREAD]: """Scans for threads using the poolscanner module and constraints. Args: @@ -77,19 +89,9 @@ class ThrdScan(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface) @classmethod def gather_thread_info( cls, - ethread: interfaces.objects.ObjectInterface, - vads_cache: Dict[int, pe_symbols.ranges_type] = None, - ) -> Tuple[ - int, - int, - int, - int, - Optional[str], - int, - Optional[str], - Optional[datetime.datetime], - Optional[datetime.datetime], - ]: + ethread: win_extensions.ETHREAD, + vads_cache: Optional[Dict[int, pe_symbols.ranges_type]] = None, + ) -> Optional[ThreadInfo]: try: thread_offset = ethread.vol.offset owner_proc_pid = ethread.Cid.UniqueProcess @@ -135,19 +137,19 @@ class ThrdScan(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface) start_path = None win32start_path = None - return ( - format_hints.Hex(thread_offset), + return cls.ThreadInfo( + thread_offset, owner_proc_pid, thread_tid, - format_hints.Hex(thread_start_addr), + thread_start_addr, start_path, - format_hints.Hex(thread_win32start_addr), + thread_win32start_addr, win32start_path, thread_create_time, thread_exit_time, ) - def _generator(self, filter_func: Callable): + def _generator(self, filter_func: Callable) -> Iterator[Tuple[int, Tuple]]: kernel_name = self.config["kernel"] vads_cache: Dict[int, pe_symbols.ranges_type] = {} @@ -156,27 +158,16 @@ class ThrdScan(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface) info = self.gather_thread_info(ethread, vads_cache) if info: - ( - offset, - pid, - tid, - start_addr, - start_path, - win32start_addr, - win32start_path, - create_time, - exit_time, - ) = info yield 0, ( - offset, - pid, - tid, - start_addr, - start_path or renderers.NotAvailableValue(), - win32start_addr, - win32start_path or renderers.NotAvailableValue(), - create_time, - exit_time, + format_hints.Hex(info.offset), + info.pid, + info.tid, + format_hints.Hex(info.start_addr), + info.start_path or renderers.NotAvailableValue(), + format_hints.Hex(info.win32_start_addr), + info.win32_start_path or renderers.NotAvailableValue(), + info.create_time, + info.exit_time, ) def generate_timeline(self): diff --git a/volatility3/framework/symbols/windows/extensions/__init__.py b/volatility3/framework/symbols/windows/extensions/__init__.py index 9fe250ba5..1972d6a50 100755 --- a/volatility3/framework/symbols/windows/extensions/__init__.py +++ b/volatility3/framework/symbols/windows/extensions/__init__.py @@ -568,16 +568,20 @@ class ETHREAD(objects.StructType, pool.ExecutiveObject): # passed all validations return True - def get_create_time(self): + def get_create_time( + self, + ) -> Union[datetime.datetime, interfaces.renderers.BaseAbsentValue]: # For Windows XPs if self.has_member("ThreadsProcess"): return conversion.wintime_to_datetime(self.CreateTime.QuadPart >> 3) return conversion.wintime_to_datetime(self.CreateTime.QuadPart) - def get_exit_time(self): + def get_exit_time( + self, + ) -> Union[datetime.datetime, interfaces.renderers.BaseAbsentValue]: return conversion.wintime_to_datetime(self.ExitTime.QuadPart) - def owning_process(self) -> interfaces.objects.ObjectInterface: + def owning_process(self) -> "EPROCESS": """Return the EPROCESS that owns this thread.""" # For Windows XPs From aba3b04e8da6c82b142b0e38011b6c50613d6823 Mon Sep 17 00:00:00 2001 From: David McDonald Date: Tue, 8 Apr 2025 14:53:20 -0500 Subject: [PATCH 017/172] Windows Thrdscan: Fix thread filtering + tracebacks Tracebacks were occurring across a number of samples when running the threads/threadscan plugins due to uncaught `InvalidAddressExceptions`. Further investigations led to the discovery of some incorrect thread filtering that was missing valid threads. --- .../framework/constants/windows/__init__.py | 2 ++ .../framework/plugins/windows/thrdscan.py | 16 +++++++++++++--- .../symbols/windows/extensions/__init__.py | 6 +++++- 3 files changed, 20 insertions(+), 4 deletions(-) diff --git a/volatility3/framework/constants/windows/__init__.py b/volatility3/framework/constants/windows/__init__.py index 6f37acd2d..b08713cc9 100644 --- a/volatility3/framework/constants/windows/__init__.py +++ b/volatility3/framework/constants/windows/__init__.py @@ -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 diff --git a/volatility3/framework/plugins/windows/thrdscan.py b/volatility3/framework/plugins/windows/thrdscan.py index 49103cc7e..162e41929 100644 --- a/volatility3/framework/plugins/windows/thrdscan.py +++ b/volatility3/framework/plugins/windows/thrdscan.py @@ -7,6 +7,7 @@ from typing import Callable, Dict, NamedTuple, Optional, Union, Tuple, Iterator from volatility3.framework import exceptions, interfaces, objects, renderers from volatility3.framework.configuration import requirements +from volatility3.framework.constants import windows as windows_constants from volatility3.framework.renderers import format_hints from volatility3.framework.symbols.windows import extensions as win_extensions from volatility3.plugins import timeliner @@ -112,10 +113,19 @@ class ThrdScan(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface) vollog.debug(f"Thread invalid address {ethread.vol.offset:#x}") return None - # don't look for VADs in kernel threads, just let them get reported with empty paths + # Filter junk PIDs if ( - owner_proc_pid != 4 - and owner_proc.InheritedFromUniqueProcessId != 4 + ethread.Cid.UniqueProcess > windows_constants.MAX_PID + or ethread.Cid.UniqueProcess == 0 + or ethread.Cid.UniqueProcess % 4 != 0 + ): + return None + + # Get VAD mappings for valid non-system (PID 4) processes + if ( + owner_proc + and owner_proc.is_valid() + and owner_proc.UniqueProcessId != 4 and vads_cache is not None ): vads = pe_symbols.PESymbols.get_vads_for_process_cache( diff --git a/volatility3/framework/symbols/windows/extensions/__init__.py b/volatility3/framework/symbols/windows/extensions/__init__.py index 1972d6a50..e6850dde5 100755 --- a/volatility3/framework/symbols/windows/extensions/__init__.py +++ b/volatility3/framework/symbols/windows/extensions/__init__.py @@ -709,7 +709,11 @@ class EPROCESS(generic.GenericIntelProcess, pool.ExecutiveObject): return False # NT pids are divisible by 4 - if self.UniqueProcessId % 4 != 0: + if ( + self.UniqueProcessId % 4 != 0 + or self.UniqueProcessId == 0 + or self.UniqueProcessId > constants.windows.MAX_PID + ): return False # check for all 0s besides the PCID entries From b35f0a29bc8eab919602acbcf5626f11c7638afd Mon Sep 17 00:00:00 2001 From: David McDonald Date: Tue, 8 Apr 2025 16:26:48 -0500 Subject: [PATCH 018/172] Windows: Remove VAD length check in thread enumeration This exclusion of threads where there are < 5 vads seems to filter valid threads (at least, threads where the start address or Win32 start address values are readable and valid disassembly, and the start time makes sense in the context of the parent process). --- .../framework/plugins/windows/thrdscan.py | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/volatility3/framework/plugins/windows/thrdscan.py b/volatility3/framework/plugins/windows/thrdscan.py index 162e41929..a3024fe58 100644 --- a/volatility3/framework/plugins/windows/thrdscan.py +++ b/volatility3/framework/plugins/windows/thrdscan.py @@ -131,17 +131,16 @@ class ThrdScan(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface) vads = pe_symbols.PESymbols.get_vads_for_process_cache( vads_cache, owner_proc ) - if not vads or len(vads) < 5: - vollog.debug( - f"Not enough vads for process at {owner_proc.vol.offset:#x}. Skipping thread at {ethread.vol.offset:#x}" - ) - return None - start_path = pe_symbols.PESymbols.filepath_for_address( - vads, thread_start_addr + start_path = ( + pe_symbols.PESymbols.filepath_for_address(vads, thread_start_addr) + if vads + else None ) - win32start_path = pe_symbols.PESymbols.filepath_for_address( - vads, thread_win32start_addr + win32start_path = ( + pe_symbols.PESymbols.filepath_for_address(vads, thread_win32start_addr) + if vads + else None ) else: start_path = None From da73a506620c73ac0642132428b5c06bd76613c3 Mon Sep 17 00:00:00 2001 From: David McDonald Date: Wed, 9 Apr 2025 21:41:01 -0500 Subject: [PATCH 019/172] Windows ThrdScan: Bump patch version number Bumping patch version due to bug fixes. --- volatility3/framework/plugins/windows/debugregisters.py | 4 ++-- .../framework/plugins/windows/orphan_kernel_threads.py | 4 ++-- volatility3/framework/plugins/windows/psxview.py | 4 ++-- volatility3/framework/plugins/windows/suspended_threads.py | 4 ++-- volatility3/framework/plugins/windows/suspicious_threads.py | 6 +++--- volatility3/framework/plugins/windows/thrdscan.py | 2 +- volatility3/framework/plugins/windows/threads.py | 4 ++-- 7 files changed, 14 insertions(+), 14 deletions(-) diff --git a/volatility3/framework/plugins/windows/debugregisters.py b/volatility3/framework/plugins/windows/debugregisters.py index 74434a3cc..40a5e31aa 100644 --- a/volatility3/framework/plugins/windows/debugregisters.py +++ b/volatility3/framework/plugins/windows/debugregisters.py @@ -24,7 +24,7 @@ vollog = logging.getLogger(__name__) class DebugRegisters(interfaces.plugins.PluginInterface): # version 2.6.0 adds support for scanning for 'Ethread' structures by pool tags _required_framework_version = (2, 6, 0) - _version = (1, 0, 1) + _version = (1, 0, 2) @classmethod def get_requirements(cls) -> List: @@ -38,7 +38,7 @@ class DebugRegisters(interfaces.plugins.PluginInterface): name="pslist", component=pslist.PsList, version=(3, 0, 0) ), requirements.VersionRequirement( - name="threads", component=threads.Threads, version=(3, 0, 0) + name="threads", component=threads.Threads, version=(3, 0, 1) ), requirements.VersionRequirement( name="pe_symbols", component=pe_symbols.PESymbols, version=(3, 0, 0) diff --git a/volatility3/framework/plugins/windows/orphan_kernel_threads.py b/volatility3/framework/plugins/windows/orphan_kernel_threads.py index b4dec0fc5..300d6069f 100644 --- a/volatility3/framework/plugins/windows/orphan_kernel_threads.py +++ b/volatility3/framework/plugins/windows/orphan_kernel_threads.py @@ -18,7 +18,7 @@ class Threads(thrdscan.ThrdScan): _required_framework_version = (2, 4, 0) # 2.0.0 - changed the signature of `list_orphan_kernel_threads` - _version = (2, 0, 0) + _version = (2, 0, 1) def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) @@ -34,7 +34,7 @@ class Threads(thrdscan.ThrdScan): architectures=["Intel32", "Intel64"], ), requirements.VersionRequirement( - name="thrdscan", component=thrdscan.ThrdScan, version=(2, 0, 0) + name="thrdscan", component=thrdscan.ThrdScan, version=(2, 0, 1) ), requirements.VersionRequirement( name="ssdt", component=ssdt.SSDT, version=(2, 0, 0) diff --git a/volatility3/framework/plugins/windows/psxview.py b/volatility3/framework/plugins/windows/psxview.py index 142987c3e..c4f4b340f 100644 --- a/volatility3/framework/plugins/windows/psxview.py +++ b/volatility3/framework/plugins/windows/psxview.py @@ -34,7 +34,7 @@ We recommend using -r pretty if you are looking at this plugin's output in a ter # 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) + _version = (1, 0, 1) valid_proc_name_chars = set( string.ascii_lowercase + string.ascii_uppercase + "." + " " @@ -55,7 +55,7 @@ We recommend using -r pretty if you are looking at this plugin's output in a ter name="psscan", component=psscan.PsScan, version=(2, 0, 0) ), requirements.VersionRequirement( - name="thrdscan", component=thrdscan.ThrdScan, version=(2, 0, 0) + name="thrdscan", component=thrdscan.ThrdScan, version=(2, 0, 1) ), requirements.VersionRequirement( name="handles", component=handles.Handles, version=(3, 0, 0) diff --git a/volatility3/framework/plugins/windows/suspended_threads.py b/volatility3/framework/plugins/windows/suspended_threads.py index 82d44a6d7..1cde0429b 100644 --- a/volatility3/framework/plugins/windows/suspended_threads.py +++ b/volatility3/framework/plugins/windows/suspended_threads.py @@ -19,7 +19,7 @@ class SuspendedThreads(interfaces.plugins.PluginInterface): """Enumerates suspended threads.""" _required_framework_version = (2, 13, 0) - _version = (1, 0, 0) + _version = (1, 0, 1) @classmethod def get_requirements(cls): @@ -36,7 +36,7 @@ class SuspendedThreads(interfaces.plugins.PluginInterface): name="pe_symbols", component=pe_symbols.PESymbols, version=(3, 0, 0) ), requirements.VersionRequirement( - name="threads", component=threads.Threads, version=(3, 0, 0) + name="threads", component=threads.Threads, version=(3, 0, 1) ), ] diff --git a/volatility3/framework/plugins/windows/suspicious_threads.py b/volatility3/framework/plugins/windows/suspicious_threads.py index eabc637c8..16da74c00 100644 --- a/volatility3/framework/plugins/windows/suspicious_threads.py +++ b/volatility3/framework/plugins/windows/suspicious_threads.py @@ -17,7 +17,7 @@ class SuspiciousThreads(interfaces.plugins.PluginInterface): """Lists suspicious userland process threads""" _required_framework_version = (2, 4, 0) - _version = (2, 0, 1) + _version = (2, 0, 2) @classmethod def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]: @@ -35,13 +35,13 @@ class SuspiciousThreads(interfaces.plugins.PluginInterface): optional=True, ), requirements.VersionRequirement( - name="thrdscan", component=thrdscan.ThrdScan, version=(2, 0, 0) + name="thrdscan", component=thrdscan.ThrdScan, version=(2, 0, 1) ), requirements.VersionRequirement( name="pslist", component=pslist.PsList, version=(3, 0, 0) ), requirements.VersionRequirement( - name="threads", component=threads.Threads, version=(3, 0, 0) + name="threads", component=threads.Threads, version=(3, 0, 1) ), requirements.VersionRequirement( name="vadinfo", component=vadinfo.VadInfo, version=(2, 0, 0) diff --git a/volatility3/framework/plugins/windows/thrdscan.py b/volatility3/framework/plugins/windows/thrdscan.py index a3024fe58..1402e7bd2 100644 --- a/volatility3/framework/plugins/windows/thrdscan.py +++ b/volatility3/framework/plugins/windows/thrdscan.py @@ -21,7 +21,7 @@ class ThrdScan(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface) # version 2.6.0 adds support for scanning for 'Ethread' structures by pool tags _required_framework_version = (2, 6, 0) - _version = (2, 0, 0) + _version = (2, 0, 1) class ThreadInfo(NamedTuple): offset: int diff --git a/volatility3/framework/plugins/windows/threads.py b/volatility3/framework/plugins/windows/threads.py index d040fa990..5531b4b24 100644 --- a/volatility3/framework/plugins/windows/threads.py +++ b/volatility3/framework/plugins/windows/threads.py @@ -16,7 +16,7 @@ class Threads(thrdscan.ThrdScan): """Lists process threads""" _required_framework_version = (2, 4, 0) - _version = (3, 0, 0) + _version = (3, 0, 1) def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) @@ -32,7 +32,7 @@ class Threads(thrdscan.ThrdScan): architectures=["Intel32", "Intel64"], ), requirements.VersionRequirement( - name="thrdscan", component=thrdscan.ThrdScan, version=(2, 0, 0) + name="thrdscan", component=thrdscan.ThrdScan, version=(2, 0, 1) ), requirements.VersionRequirement( name="pslist", component=pslist.PsList, version=(3, 0, 0) From f091641920cda3a285ae2b633a8412e38f9501cc Mon Sep 17 00:00:00 2001 From: David McDonald Date: Wed, 9 Apr 2025 21:50:36 -0500 Subject: [PATCH 020/172] Framework: Bump patch version number Bumping the framework patch version number due to bugfix in windows' `EPROCESS` extension class' `is_valid()` method. --- volatility3/framework/constants/_version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/volatility3/framework/constants/_version.py b/volatility3/framework/constants/_version.py index a299f15a2..07b9e45ec 100644 --- a/volatility3/framework/constants/_version.py +++ b/volatility3/framework/constants/_version.py @@ -1,7 +1,7 @@ # We use the SemVer 2.0.0 versioning scheme VERSION_MAJOR = 2 # Number of releases of the library with a breaking change VERSION_MINOR = 26 # Number of changes that only add to the interface -VERSION_PATCH = 1 # 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 = ( From 7192730deacfd2e17531215a597d93bc7e015f19 Mon Sep 17 00:00:00 2001 From: David McDonald Date: Wed, 9 Apr 2025 22:53:49 -0500 Subject: [PATCH 021/172] Layers: Fix exception message Updates the exception message to report the correct dependency instead of the layer name itself. Instead of reporting the actual dependency, it was reporting, for example 'Layer layer_name is depended upon by layer_name'. --- volatility3/framework/interfaces/layers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/volatility3/framework/interfaces/layers.py b/volatility3/framework/interfaces/layers.py index a90a78667..6c4e4b419 100644 --- a/volatility3/framework/interfaces/layers.py +++ b/volatility3/framework/interfaces/layers.py @@ -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() From 6237950991e7d78718bd6931c1598afac325043f Mon Sep 17 00:00:00 2001 From: David McDonald Date: Thu, 10 Apr 2025 09:50:49 -0500 Subject: [PATCH 022/172] Windows: Revert unneeded version bumps --- volatility3/framework/plugins/windows/debugregisters.py | 4 ++-- .../framework/plugins/windows/orphan_kernel_threads.py | 4 ++-- volatility3/framework/plugins/windows/psxview.py | 4 ++-- volatility3/framework/plugins/windows/suspended_threads.py | 4 ++-- volatility3/framework/plugins/windows/suspicious_threads.py | 6 +++--- volatility3/framework/plugins/windows/threads.py | 4 ++-- 6 files changed, 13 insertions(+), 13 deletions(-) diff --git a/volatility3/framework/plugins/windows/debugregisters.py b/volatility3/framework/plugins/windows/debugregisters.py index 40a5e31aa..74434a3cc 100644 --- a/volatility3/framework/plugins/windows/debugregisters.py +++ b/volatility3/framework/plugins/windows/debugregisters.py @@ -24,7 +24,7 @@ vollog = logging.getLogger(__name__) class DebugRegisters(interfaces.plugins.PluginInterface): # version 2.6.0 adds support for scanning for 'Ethread' structures by pool tags _required_framework_version = (2, 6, 0) - _version = (1, 0, 2) + _version = (1, 0, 1) @classmethod def get_requirements(cls) -> List: @@ -38,7 +38,7 @@ class DebugRegisters(interfaces.plugins.PluginInterface): name="pslist", component=pslist.PsList, version=(3, 0, 0) ), requirements.VersionRequirement( - name="threads", component=threads.Threads, version=(3, 0, 1) + name="threads", component=threads.Threads, version=(3, 0, 0) ), requirements.VersionRequirement( name="pe_symbols", component=pe_symbols.PESymbols, version=(3, 0, 0) diff --git a/volatility3/framework/plugins/windows/orphan_kernel_threads.py b/volatility3/framework/plugins/windows/orphan_kernel_threads.py index 300d6069f..b4dec0fc5 100644 --- a/volatility3/framework/plugins/windows/orphan_kernel_threads.py +++ b/volatility3/framework/plugins/windows/orphan_kernel_threads.py @@ -18,7 +18,7 @@ class Threads(thrdscan.ThrdScan): _required_framework_version = (2, 4, 0) # 2.0.0 - changed the signature of `list_orphan_kernel_threads` - _version = (2, 0, 1) + _version = (2, 0, 0) def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) @@ -34,7 +34,7 @@ class Threads(thrdscan.ThrdScan): architectures=["Intel32", "Intel64"], ), requirements.VersionRequirement( - name="thrdscan", component=thrdscan.ThrdScan, version=(2, 0, 1) + name="thrdscan", component=thrdscan.ThrdScan, version=(2, 0, 0) ), requirements.VersionRequirement( name="ssdt", component=ssdt.SSDT, version=(2, 0, 0) diff --git a/volatility3/framework/plugins/windows/psxview.py b/volatility3/framework/plugins/windows/psxview.py index c4f4b340f..142987c3e 100644 --- a/volatility3/framework/plugins/windows/psxview.py +++ b/volatility3/framework/plugins/windows/psxview.py @@ -34,7 +34,7 @@ We recommend using -r pretty if you are looking at this plugin's output in a ter # 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, 1) + _version = (1, 0, 0) valid_proc_name_chars = set( string.ascii_lowercase + string.ascii_uppercase + "." + " " @@ -55,7 +55,7 @@ We recommend using -r pretty if you are looking at this plugin's output in a ter name="psscan", component=psscan.PsScan, version=(2, 0, 0) ), requirements.VersionRequirement( - name="thrdscan", component=thrdscan.ThrdScan, version=(2, 0, 1) + name="thrdscan", component=thrdscan.ThrdScan, version=(2, 0, 0) ), requirements.VersionRequirement( name="handles", component=handles.Handles, version=(3, 0, 0) diff --git a/volatility3/framework/plugins/windows/suspended_threads.py b/volatility3/framework/plugins/windows/suspended_threads.py index 1cde0429b..82d44a6d7 100644 --- a/volatility3/framework/plugins/windows/suspended_threads.py +++ b/volatility3/framework/plugins/windows/suspended_threads.py @@ -19,7 +19,7 @@ class SuspendedThreads(interfaces.plugins.PluginInterface): """Enumerates suspended threads.""" _required_framework_version = (2, 13, 0) - _version = (1, 0, 1) + _version = (1, 0, 0) @classmethod def get_requirements(cls): @@ -36,7 +36,7 @@ class SuspendedThreads(interfaces.plugins.PluginInterface): name="pe_symbols", component=pe_symbols.PESymbols, version=(3, 0, 0) ), requirements.VersionRequirement( - name="threads", component=threads.Threads, version=(3, 0, 1) + name="threads", component=threads.Threads, version=(3, 0, 0) ), ] diff --git a/volatility3/framework/plugins/windows/suspicious_threads.py b/volatility3/framework/plugins/windows/suspicious_threads.py index 16da74c00..eabc637c8 100644 --- a/volatility3/framework/plugins/windows/suspicious_threads.py +++ b/volatility3/framework/plugins/windows/suspicious_threads.py @@ -17,7 +17,7 @@ class SuspiciousThreads(interfaces.plugins.PluginInterface): """Lists suspicious userland process threads""" _required_framework_version = (2, 4, 0) - _version = (2, 0, 2) + _version = (2, 0, 1) @classmethod def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]: @@ -35,13 +35,13 @@ class SuspiciousThreads(interfaces.plugins.PluginInterface): optional=True, ), requirements.VersionRequirement( - name="thrdscan", component=thrdscan.ThrdScan, version=(2, 0, 1) + 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, 1) + name="threads", component=threads.Threads, version=(3, 0, 0) ), requirements.VersionRequirement( name="vadinfo", component=vadinfo.VadInfo, version=(2, 0, 0) diff --git a/volatility3/framework/plugins/windows/threads.py b/volatility3/framework/plugins/windows/threads.py index 5531b4b24..d040fa990 100644 --- a/volatility3/framework/plugins/windows/threads.py +++ b/volatility3/framework/plugins/windows/threads.py @@ -16,7 +16,7 @@ class Threads(thrdscan.ThrdScan): """Lists process threads""" _required_framework_version = (2, 4, 0) - _version = (3, 0, 1) + _version = (3, 0, 0) def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) @@ -32,7 +32,7 @@ class Threads(thrdscan.ThrdScan): architectures=["Intel32", "Intel64"], ), requirements.VersionRequirement( - name="thrdscan", component=thrdscan.ThrdScan, version=(2, 0, 1) + name="thrdscan", component=thrdscan.ThrdScan, version=(2, 0, 0) ), requirements.VersionRequirement( name="pslist", component=pslist.PsList, version=(3, 0, 0) From b82458e365149ee9255235be1751b6559fa8199c Mon Sep 17 00:00:00 2001 From: David McDonald Date: Thu, 10 Apr 2025 09:51:32 -0500 Subject: [PATCH 023/172] Windows Thrdscan: Convert from PATCH to MINOR version bump --- volatility3/framework/plugins/windows/thrdscan.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/volatility3/framework/plugins/windows/thrdscan.py b/volatility3/framework/plugins/windows/thrdscan.py index 1402e7bd2..d5a1a0b07 100644 --- a/volatility3/framework/plugins/windows/thrdscan.py +++ b/volatility3/framework/plugins/windows/thrdscan.py @@ -21,7 +21,7 @@ class ThrdScan(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface) # version 2.6.0 adds support for scanning for 'Ethread' structures by pool tags _required_framework_version = (2, 6, 0) - _version = (2, 0, 1) + _version = (2, 1, 0) class ThreadInfo(NamedTuple): offset: int From c48ad901b03c72e7c1ceffe3712dc8322c19de38 Mon Sep 17 00:00:00 2001 From: David McDonald Date: Thu, 10 Apr 2025 14:49:50 -0500 Subject: [PATCH 024/172] Extensions: Add cache decorators as appropriate I noticed that many plugins were creating duplicate per-process translation layers - for instance, `windows.envars.Envars` was ending up with > 30 process layers per process due to repeated calls to `get_peb()`, which calls `add_process_space()` internally. This adds `@functools.lru_cache` to the `get_peb()` and `get_peb32()` methods on the `EPROCESS` extension, since these should only need to be created once. Also adds `@functools.lru_cache` to `add_process_space` to enable reusing the same process address space, provided the same arguments are passed to the `add_process_space()` method. --- volatility3/framework/symbols/linux/extensions/__init__.py | 1 + volatility3/framework/symbols/mac/extensions/__init__.py | 2 ++ volatility3/framework/symbols/windows/extensions/__init__.py | 5 ++++- 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/volatility3/framework/symbols/linux/extensions/__init__.py b/volatility3/framework/symbols/linux/extensions/__init__.py index b4bde1aba..d281e2b9b 100644 --- a/volatility3/framework/symbols/linux/extensions/__init__.py +++ b/volatility3/framework/symbols/linux/extensions/__init__.py @@ -471,6 +471,7 @@ class task_struct(generic.GenericIntelProcess): return True + @functools.lru_cache def add_process_layer( self, config_prefix: Optional[str] = None, preferred_name: Optional[str] = None ) -> Optional[str]: diff --git a/volatility3/framework/symbols/mac/extensions/__init__.py b/volatility3/framework/symbols/mac/extensions/__init__.py index cc700f209..08ec63afa 100644 --- a/volatility3/framework/symbols/mac/extensions/__init__.py +++ b/volatility3/framework/symbols/mac/extensions/__init__.py @@ -2,6 +2,7 @@ # which is available at https://www.volatilityfoundation.org/license/vsl-v1.0 # import contextlib +import functools import logging from typing import Generator, Iterable, Optional, Set, Tuple @@ -17,6 +18,7 @@ class proc(generic.GenericIntelProcess): def get_task(self): return self.task.dereference().cast("task") + @functools.lru_cache def add_process_layer( self, config_prefix: Optional[str] = None, preferred_name: Optional[str] = None ) -> Optional[str]: diff --git a/volatility3/framework/symbols/windows/extensions/__init__.py b/volatility3/framework/symbols/windows/extensions/__init__.py index 9fe250ba5..b90391af3 100755 --- a/volatility3/framework/symbols/windows/extensions/__init__.py +++ b/volatility3/framework/symbols/windows/extensions/__init__.py @@ -732,9 +732,10 @@ class EPROCESS(generic.GenericIntelProcess, pool.ExecutiveObject): return True + @functools.lru_cache def add_process_layer( self, config_prefix: Optional[str] = None, preferred_name: Optional[str] = None - ): + ) -> str: """Constructs a new layer based on the process's DirectoryTableBase.""" parent_layer = self._context.layers[self.vol.layer_name] @@ -761,6 +762,7 @@ class EPROCESS(generic.GenericIntelProcess, pool.ExecutiveObject): self._context, dtb, config_prefix, preferred_name ) + @functools.lru_cache def get_peb(self) -> interfaces.objects.ObjectInterface: """Constructs a PEB object""" if constants.BANG not in self.vol.type_name: @@ -786,6 +788,7 @@ class EPROCESS(generic.GenericIntelProcess, pool.ExecutiveObject): ) return peb + @functools.lru_cache def get_peb32(self) -> Optional[interfaces.objects.ObjectInterface]: """Constructs a PEB32 object""" if constants.BANG not in self.vol.type_name: From 03e750648f6068955cbed9e4dcf2fd2729b8c3ef Mon Sep 17 00:00:00 2001 From: David McDonald Date: Mon, 14 Apr 2025 09:20:32 -0500 Subject: [PATCH 025/172] Windows Extensions: remove lru_cache decorators These don't offer enough upside to be worthwhile compared to the decorators applied to the `add_process_layer` methods. --- volatility3/framework/symbols/windows/extensions/__init__.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/volatility3/framework/symbols/windows/extensions/__init__.py b/volatility3/framework/symbols/windows/extensions/__init__.py index b90391af3..40f4e75e3 100755 --- a/volatility3/framework/symbols/windows/extensions/__init__.py +++ b/volatility3/framework/symbols/windows/extensions/__init__.py @@ -762,7 +762,6 @@ class EPROCESS(generic.GenericIntelProcess, pool.ExecutiveObject): self._context, dtb, config_prefix, preferred_name ) - @functools.lru_cache def get_peb(self) -> interfaces.objects.ObjectInterface: """Constructs a PEB object""" if constants.BANG not in self.vol.type_name: @@ -788,7 +787,6 @@ class EPROCESS(generic.GenericIntelProcess, pool.ExecutiveObject): ) return peb - @functools.lru_cache def get_peb32(self) -> Optional[interfaces.objects.ObjectInterface]: """Constructs a PEB32 object""" if constants.BANG not in self.vol.type_name: From 87cc571fbba8d6d04aecda758f36729fba44b0ec Mon Sep 17 00:00:00 2001 From: David McDonald Date: Mon, 14 Apr 2025 10:18:17 -0500 Subject: [PATCH 026/172] ShimcacheMem: Fix symbol table These SHIM_CACHE_ENTRY offsets aren't correct - should be identical to those in the other XP symbol tables. --- .../windows/shimcache/shimcache-xp-sp2-x86.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/volatility3/framework/symbols/windows/shimcache/shimcache-xp-sp2-x86.json b/volatility3/framework/symbols/windows/shimcache/shimcache-xp-sp2-x86.json index 6114e6c85..6c990f9f5 100644 --- a/volatility3/framework/symbols/windows/shimcache/shimcache-xp-sp2-x86.json +++ b/volatility3/framework/symbols/windows/shimcache/shimcache-xp-sp2-x86.json @@ -331,23 +331,23 @@ "LastModified": { "type": { "kind": "union", - "name": "LARGE_INTEGER" + "name": "_LARGE_INTEGER" }, - "offset": 4 + "offset": 528 }, "FileSize": { "type": { "kind": "base", "name": "long long" }, - "offset": 8 + "offset": 536 }, "LastUpdate": { "type": { "kind": "union", - "name": "LARGE_INTEGER" + "name": "_LARGE_INTEGER" }, - "offset": 12 + "offset": 544 } }, "kind": "struct", From b80c52873b62a325453b9b443446e7f2ef4ad575 Mon Sep 17 00:00:00 2001 From: David McDonald Date: Mon, 14 Apr 2025 10:19:01 -0500 Subject: [PATCH 027/172] ShimcacheMem: Use `address_to_string` Uses the new `address_to_string` utility function to read filepaths, in case a filepath crosses a boundary to a swapped page. --- .../framework/symbols/windows/extensions/shimcache.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/volatility3/framework/symbols/windows/extensions/shimcache.py b/volatility3/framework/symbols/windows/extensions/shimcache.py index b84a7df6f..279a1995c 100644 --- a/volatility3/framework/symbols/windows/extensions/shimcache.py +++ b/volatility3/framework/symbols/windows/extensions/shimcache.py @@ -8,6 +8,7 @@ from datetime import datetime from typing import Dict, Optional, Tuple, Union from volatility3.framework import constants, exceptions, interfaces, objects, renderers +from volatility3.framework.objects.utility import address_to_string from volatility3.framework.symbols.windows.extensions import conversion vollog = logging.getLogger(__name__) @@ -126,8 +127,12 @@ class SHIM_CACHE_ENTRY(objects.StructType): return self._file_path if not hasattr(self.Path, "Buffer"): - return self.Path.cast( - "string", max_length=self.Path.vol.count, encoding="utf-16le" + return address_to_string( + self._context, + self.Path.vol.layer_name, + self.Path.vol.offset, + 520, + encoding="utf-16le", ) try: From 079d2801f51f4191996ec9d2a1ace4edb9d956a9 Mon Sep 17 00:00:00 2001 From: David McDonald Date: Mon, 14 Apr 2025 10:19:55 -0500 Subject: [PATCH 028/172] ShimcacheMem: Fix offset tracking This fixes a bug in the plugin logic that causes valid entries to be excluded in the following scenario: - An entry is discovered but deemed invalid due to unreadable size/timestamps. - The offset gets placed into the `seen` tracking set anyway - Another entry (this time, with valid filesize/timestamps) with the same physical offset is encountered, but is skipped because this offset is already in the `seen` tracking set. This updates the logic to only add the offset to the tracker if the shimcache entry is valid. --- volatility3/framework/plugins/windows/shimcachemem.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/volatility3/framework/plugins/windows/shimcachemem.py b/volatility3/framework/plugins/windows/shimcachemem.py index 7883dfba3..f60e5c5cf 100644 --- a/volatility3/framework/plugins/windows/shimcachemem.py +++ b/volatility3/framework/plugins/windows/shimcachemem.py @@ -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 From 1d4893156e04b0feb94249f1dc5536cb39ea2a75 Mon Sep 17 00:00:00 2001 From: David McDonald Date: Mon, 14 Apr 2025 11:13:03 -0500 Subject: [PATCH 029/172] Shimcache Extension: Fix logic error in `exec_flag` Adds needed return statements in `exec_flag` method in order to avoid `InvalidAddressException` when reading from invalid memory after an `is_valid` check has already been performed. --- .../framework/symbols/windows/extensions/shimcache.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/volatility3/framework/symbols/windows/extensions/shimcache.py b/volatility3/framework/symbols/windows/extensions/shimcache.py index 279a1995c..a80ecc0ea 100644 --- a/volatility3/framework/symbols/windows/extensions/shimcache.py +++ b/volatility3/framework/symbols/windows/extensions/shimcache.py @@ -57,12 +57,14 @@ class SHIM_CACHE_ENTRY(objects.StructType): blob_offset, blob_size ): self._exec_flag = renderers.UnparsableValue() + return self._exec_flag raw_flag = self._context.layers[self.vol.native_layer_name].read( blob_offset, blob_size ) if not raw_flag: self._exec_flag = renderers.UnparsableValue() + return self._exec_flag try: self._exec_flag = bool(struct.unpack(" Date: Mon, 14 Apr 2025 11:41:00 -0500 Subject: [PATCH 030/172] Shimcache: Use `self.Path.vol.count` instead of hardcoded value --- volatility3/framework/symbols/windows/extensions/shimcache.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/volatility3/framework/symbols/windows/extensions/shimcache.py b/volatility3/framework/symbols/windows/extensions/shimcache.py index a80ecc0ea..2e7c68fa9 100644 --- a/volatility3/framework/symbols/windows/extensions/shimcache.py +++ b/volatility3/framework/symbols/windows/extensions/shimcache.py @@ -136,7 +136,7 @@ class SHIM_CACHE_ENTRY(objects.StructType): self._context, self.Path.vol.layer_name, self.Path.vol.offset, - 520, + self.Path.vol.count, encoding="utf-16le", ) From 17f9d218c93fd47ea491e0a7ce0ae746a46ca092 Mon Sep 17 00:00:00 2001 From: David McDonald Date: Mon, 14 Apr 2025 16:17:20 -0500 Subject: [PATCH 031/172] Windows PsList: Fix list traversal logic The traversal of `ActiveProcessLinks` from `PsActiveProcessHead` was only being done in the forward direction; if for some reason `PsActiveProcessHead` hasn't been updated to point at the 'current' list head, entries in the backwards traversal direction will be missed. --- volatility3/framework/plugins/windows/pslist.py | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/volatility3/framework/plugins/windows/pslist.py b/volatility3/framework/plugins/windows/pslist.py index b92fdf66c..fb28b1843 100644 --- a/volatility3/framework/plugins/windows/pslist.py +++ b/volatility3/framework/plugins/windows/pslist.py @@ -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__) @@ -261,9 +261,16 @@ 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( + eproc.vol.type_name, "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"]] From 8f9bebdc86af17b02c3efa8b07ed0ec96d5cc442 Mon Sep 17 00:00:00 2001 From: David McDonald Date: Mon, 14 Apr 2025 17:10:22 -0500 Subject: [PATCH 032/172] Linux PsList: Fix process list traversal Fixes linux process listing by traversing the list backwards as well as forwards while tracking seen process offsets to avoid duplicates. --- volatility3/framework/plugins/linux/pslist.py | 26 +++++++++++++------ 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/volatility3/framework/plugins/linux/pslist.py b/volatility3/framework/plugins/linux/pslist.py index 2f0cc00b7..28009ec06 100644 --- a/volatility3/framework/plugins/linux/pslist.py +++ b/volatility3/framework/plugins/linux/pslist.py @@ -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") From 91e23e0c71d0471fcc1f8bd6a359925386a5c6be Mon Sep 17 00:00:00 2001 From: David McDonald Date: Mon, 14 Apr 2025 17:15:28 -0500 Subject: [PATCH 033/172] Bumps patch version for pslist plugins --- volatility3/framework/plugins/linux/pslist.py | 2 +- volatility3/framework/plugins/windows/pslist.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/volatility3/framework/plugins/linux/pslist.py b/volatility3/framework/plugins/linux/pslist.py index 28009ec06..9b31976ec 100644 --- a/volatility3/framework/plugins/linux/pslist.py +++ b/volatility3/framework/plugins/linux/pslist.py @@ -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]: diff --git a/volatility3/framework/plugins/windows/pslist.py b/volatility3/framework/plugins/windows/pslist.py index fb28b1843..0048730ea 100644 --- a/volatility3/framework/plugins/windows/pslist.py +++ b/volatility3/framework/plugins/windows/pslist.py @@ -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 From 2e206fe698a7cdf7f5fd938930df148dae8711ce Mon Sep 17 00:00:00 2001 From: David McDonald Date: Mon, 14 Apr 2025 17:17:02 -0500 Subject: [PATCH 034/172] Add keywords to parameters --- volatility3/framework/plugins/windows/pslist.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/volatility3/framework/plugins/windows/pslist.py b/volatility3/framework/plugins/windows/pslist.py index 0048730ea..1043f8b42 100644 --- a/volatility3/framework/plugins/windows/pslist.py +++ b/volatility3/framework/plugins/windows/pslist.py @@ -264,7 +264,9 @@ class PsList(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface): seen = set() for forward in (True, False): for proc in eproc.ActiveProcessLinks.to_list( - eproc.vol.type_name, "ActiveProcessLinks", forward=forward + symbol_type=eproc.vol.type_name, + member="ActiveProcessLinks", + forward=forward, ): if proc.vol.offset in seen: continue From 3b723f8be38596623b5a50fd5d8139960c7c5598 Mon Sep 17 00:00:00 2001 From: David McDonald Date: Thu, 10 Apr 2025 15:43:21 -0500 Subject: [PATCH 035/172] Linux pagecache.Files: Memory Usage Converts `objects.Pointer` to `int` before storing them in the set. This should have a substantial impact on memory, similar to those in #1758 --- volatility3/framework/plugins/linux/mountinfo.py | 4 ++-- volatility3/framework/plugins/linux/pagecache.py | 13 +++++++------ 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/volatility3/framework/plugins/linux/mountinfo.py b/volatility3/framework/plugins/linux/mountinfo.py index 668b039db..9790c4b57 100644 --- a/volatility3/framework/plugins/linux/mountinfo.py +++ b/volatility3/framework/plugins/linux/mountinfo.py @@ -279,9 +279,9 @@ class MountInfo(plugins.PluginInterface): if not (sb_ptr and sb_ptr.is_readable()): continue - if sb_ptr in seen_sb_ptr: + if int(sb_ptr) in seen_sb_ptr: continue - seen_sb_ptr.add(sb_ptr) + seen_sb_ptr.add(int(sb_ptr)) superblock = sb_ptr.dereference() diff --git a/volatility3/framework/plugins/linux/pagecache.py b/volatility3/framework/plugins/linux/pagecache.py index 60b8b066b..6c27c94b2 100644 --- a/volatility3/framework/plugins/linux/pagecache.py +++ b/volatility3/framework/plugins/linux/pagecache.py @@ -204,10 +204,10 @@ class Files(plugins.PluginInterface, timeliner.TimeLinerInterface): if dentry_addr == root_dentry.vol.offset: continue - if dentry_addr in seen_dentries: + if int(dentry_addr) in seen_dentries: continue - seen_dentries.add(dentry_addr) + seen_dentries.add(int(dentry_addr)) inode_ptr = dentry.d_inode if not (inode_ptr and inode_ptr.is_readable()): @@ -283,9 +283,10 @@ class Files(plugins.PluginInterface, timeliner.TimeLinerInterface): continue # Inode already processed? - if root_inode_ptr in seen_inodes: + if int(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 +319,9 @@ class Files(plugins.PluginInterface, timeliner.TimeLinerInterface): continue # Inode already processed? - if file_inode_ptr in seen_inodes: + if int(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) From c641690c5366bd0c65864438e87157b6dc9218ce Mon Sep 17 00:00:00 2001 From: David McDonald Date: Mon, 14 Apr 2025 09:10:38 -0500 Subject: [PATCH 036/172] Pagecache: revert cast to int `dentry.vol.offset` is already a basic Python `int`. --- volatility3/framework/plugins/linux/pagecache.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/volatility3/framework/plugins/linux/pagecache.py b/volatility3/framework/plugins/linux/pagecache.py index 6c27c94b2..7da42255d 100644 --- a/volatility3/framework/plugins/linux/pagecache.py +++ b/volatility3/framework/plugins/linux/pagecache.py @@ -204,10 +204,10 @@ class Files(plugins.PluginInterface, timeliner.TimeLinerInterface): if dentry_addr == root_dentry.vol.offset: continue - if int(dentry_addr) in seen_dentries: + if dentry_addr in seen_dentries: continue - seen_dentries.add(int(dentry_addr)) + seen_dentries.add(dentry_addr) inode_ptr = dentry.d_inode if not (inode_ptr and inode_ptr.is_readable()): From 9ceec51b76711419d89ccf5ff1ae0b25a5b36f68 Mon Sep 17 00:00:00 2001 From: David McDonald Date: Mon, 14 Apr 2025 09:11:24 -0500 Subject: [PATCH 037/172] Pagecache: Add comments explaining cast Adds a couple of comments explaining why we're doing a lossy conversion to Python `int` (saving memory). --- volatility3/framework/plugins/linux/pagecache.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/volatility3/framework/plugins/linux/pagecache.py b/volatility3/framework/plugins/linux/pagecache.py index 7da42255d..87bd20b68 100644 --- a/volatility3/framework/plugins/linux/pagecache.py +++ b/volatility3/framework/plugins/linux/pagecache.py @@ -283,6 +283,10 @@ 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 int(root_inode_ptr) in seen_inodes: continue @@ -319,6 +323,10 @@ 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 int(file_inode_ptr) in seen_inodes: continue seen_inodes.add(int(file_inode_ptr)) From a236c0dd40a640850e6e9408c1dfbce502c4346b Mon Sep 17 00:00:00 2001 From: David McDonald Date: Mon, 14 Apr 2025 18:30:19 -0500 Subject: [PATCH 038/172] linux.pagecache.Files: Trim unneeded cast Removes casts to `int` performed before checking set membership, since the computed `__hash__` value will be the same for both the Python primitive and the volatility `objects.Pointer`. --- volatility3/framework/plugins/linux/pagecache.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/volatility3/framework/plugins/linux/pagecache.py b/volatility3/framework/plugins/linux/pagecache.py index 87bd20b68..1fd96d5d2 100644 --- a/volatility3/framework/plugins/linux/pagecache.py +++ b/volatility3/framework/plugins/linux/pagecache.py @@ -287,7 +287,7 @@ class Files(plugins.PluginInterface, timeliner.TimeLinerInterface): # 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 int(root_inode_ptr) in seen_inodes: + if root_inode_ptr in seen_inodes: continue seen_inodes.add(int(root_inode_ptr)) @@ -327,7 +327,7 @@ class Files(plugins.PluginInterface, timeliner.TimeLinerInterface): # 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 int(file_inode_ptr) in seen_inodes: + if file_inode_ptr in seen_inodes: continue seen_inodes.add(int(file_inode_ptr)) From d29be5cbdea0ac75d4895588f98e92198408d5e0 Mon Sep 17 00:00:00 2001 From: David McDonald Date: Mon, 14 Apr 2025 18:38:47 -0500 Subject: [PATCH 039/172] Linux MountInfo: Remove unneeded cast --- volatility3/framework/plugins/linux/mountinfo.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/volatility3/framework/plugins/linux/mountinfo.py b/volatility3/framework/plugins/linux/mountinfo.py index 9790c4b57..f00733a54 100644 --- a/volatility3/framework/plugins/linux/mountinfo.py +++ b/volatility3/framework/plugins/linux/mountinfo.py @@ -279,7 +279,7 @@ class MountInfo(plugins.PluginInterface): if not (sb_ptr and sb_ptr.is_readable()): continue - if int(sb_ptr) in seen_sb_ptr: + if sb_ptr in seen_sb_ptr: continue seen_sb_ptr.add(int(sb_ptr)) From 02fbb3ce4933349edd90e170515268c23e610bc5 Mon Sep 17 00:00:00 2001 From: David McDonald Date: Tue, 15 Apr 2025 10:14:58 -0500 Subject: [PATCH 040/172] Poolscanners: Fix symlink pool types Fixes regression introduced in #1632 Symbolic links are allocated in the paged pools, not non-paged. This was causing us to miss symlinks across both pre and post win8 samples. --- volatility3/framework/plugins/windows/poolscanner.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/volatility3/framework/plugins/windows/poolscanner.py b/volatility3/framework/plugins/windows/poolscanner.py index 7929b70e4..f62a34962 100644 --- a/volatility3/framework/plugins/windows/poolscanner.py +++ b/volatility3/framework/plugins/windows/poolscanner.py @@ -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( From d745a62d7faa7cb91f2b77ca96b031d09a044b2b Mon Sep 17 00:00:00 2001 From: David McDonald Date: Tue, 15 Apr 2025 10:21:52 -0500 Subject: [PATCH 041/172] PoolScanner: Patch version bump Bumping the patch version due to bugfix. --- volatility3/framework/plugins/windows/poolscanner.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/volatility3/framework/plugins/windows/poolscanner.py b/volatility3/framework/plugins/windows/poolscanner.py index f62a34962..7030b4c9a 100644 --- a/volatility3/framework/plugins/windows/poolscanner.py +++ b/volatility3/framework/plugins/windows/poolscanner.py @@ -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]: From 32e9cee6dd64f41145d723f0a864548d995d5fc7 Mon Sep 17 00:00:00 2001 From: David McDonald Date: Tue, 15 Apr 2025 10:43:12 -0500 Subject: [PATCH 042/172] Tests: Add Symlinkscan generic test This should be enough to prevent serious regressions that break all output. --- test/plugins/windows/windows.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/test/plugins/windows/windows.py b/test/plugins/windows/windows.py index 6733d543e..0db3aa35b 100644 --- a/test/plugins/windows/windows.py +++ b/test/plugins/windows/windows.py @@ -746,6 +746,19 @@ 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 + + class TestWindowsLdrModules: def test_windows_specific_ldrmodules(self, volatility, python): image = WindowsSamples.WINDOWSXP_GENERIC.value.path From 2199375dd52af3913eb4abf7fe5e05ac6c625941 Mon Sep 17 00:00:00 2001 From: David McDonald Date: Tue, 15 Apr 2025 10:48:49 -0500 Subject: [PATCH 043/172] Tests: Add symlinkscan specific test --- test/plugins/windows/windows.py | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/test/plugins/windows/windows.py b/test/plugins/windows/windows.py index 0db3aa35b..4272b64d2 100644 --- a/test/plugins/windows/windows.py +++ b/test/plugins/windows/windows.py @@ -758,6 +758,38 @@ class TestWindowsSymlinkScan: 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): From 4ce173e87a868054f9ad3823f0f4a0c84e757731 Mon Sep 17 00:00:00 2001 From: David McDonald Date: Tue, 15 Apr 2025 15:52:54 -0500 Subject: [PATCH 044/172] ShimcacheMem: Fix traceback in extension method When performing the attribute checks for ListFlags.BlobBuffer, a pointer dereference occurs implicitly that can trigger an `exceptions.InvalidAddressException`. This wraps the checks in a try/except block, and sets the value of `_exec_flag` to `renderers.UnreadableValue` if one occurs. --- .../symbols/windows/extensions/shimcache.py | 63 ++++++++++--------- 1 file changed, 35 insertions(+), 28 deletions(-) diff --git a/volatility3/framework/symbols/windows/extensions/shimcache.py b/volatility3/framework/symbols/windows/extensions/shimcache.py index 2e7c68fa9..9e75e7579 100644 --- a/volatility3/framework/symbols/windows/extensions/shimcache.py +++ b/volatility3/framework/symbols/windows/extensions/shimcache.py @@ -39,37 +39,44 @@ class SHIM_CACHE_ENTRY(objects.StructType): if self._exec_flag is not None: return self._exec_flag - if hasattr(self, "ListEntryDetail") and hasattr( - self.ListEntryDetail, "InsertFlags" - ): - self._exec_flag = self.ListEntryDetail.InsertFlags & 0x2 == 2 - - elif hasattr(self, "InsertFlags"): - self._exec_flag = self.InsertFlags & 0x2 == 2 - - elif hasattr(self, "ListEntryDetail") and hasattr( - self.ListEntryDetail, "BlobBuffer" - ): - blob_offset = self.ListEntryDetail.BlobBuffer - blob_size = self.ListEntryDetail.BlobSize - - if not self._context.layers[self.vol.native_layer_name].is_valid( - blob_offset, blob_size + try: + if hasattr(self, "ListEntryDetail") and hasattr( + self.ListEntryDetail, "InsertFlags" ): - self._exec_flag = renderers.UnparsableValue() - return self._exec_flag + self._exec_flag = self.ListEntryDetail.InsertFlags & 0x2 == 2 - raw_flag = self._context.layers[self.vol.native_layer_name].read( - blob_offset, blob_size + elif hasattr(self, "InsertFlags"): + self._exec_flag = self.InsertFlags & 0x2 == 2 + + elif hasattr(self, "ListEntryDetail") and hasattr( + self.ListEntryDetail, "BlobBuffer" + ): + blob_offset = self.ListEntryDetail.BlobBuffer + blob_size = self.ListEntryDetail.BlobSize + + if not self._context.layers[self.vol.native_layer_name].is_valid( + blob_offset, blob_size + ): + self._exec_flag = renderers.UnparsableValue() + return self._exec_flag + + raw_flag = self._context.layers[self.vol.native_layer_name].read( + blob_offset, blob_size + ) + if not raw_flag: + self._exec_flag = renderers.UnparsableValue() + return self._exec_flag + + try: + self._exec_flag = bool(struct.unpack(" Date: Tue, 15 Apr 2025 15:55:52 -0500 Subject: [PATCH 045/172] Shimcachemem: Changes absent value return type This is more appropriately set to `renderers.UnreadableValue` since it is set when an `exceptions.InvalidAddressException` occurs. --- volatility3/framework/symbols/windows/extensions/shimcache.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/volatility3/framework/symbols/windows/extensions/shimcache.py b/volatility3/framework/symbols/windows/extensions/shimcache.py index 9e75e7579..3b32d30c7 100644 --- a/volatility3/framework/symbols/windows/extensions/shimcache.py +++ b/volatility3/framework/symbols/windows/extensions/shimcache.py @@ -57,7 +57,7 @@ class SHIM_CACHE_ENTRY(objects.StructType): if not self._context.layers[self.vol.native_layer_name].is_valid( blob_offset, blob_size ): - self._exec_flag = renderers.UnparsableValue() + self._exec_flag = renderers.UnreadableValue() return self._exec_flag raw_flag = self._context.layers[self.vol.native_layer_name].read( From 330e19f1594a70437be1290e9c71829461942fed Mon Sep 17 00:00:00 2001 From: Abyss Watcher Date: Tue, 15 Apr 2025 23:04:49 +0200 Subject: [PATCH 046/172] call parent get_requirements() --- volatility3/framework/layers/qemu.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/volatility3/framework/layers/qemu.py b/volatility3/framework/layers/qemu.py index eb44de347..0ab359559 100644 --- a/volatility3/framework/layers/qemu.py +++ b/volatility3/framework/layers/qemu.py @@ -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, From 4c6188766c5b453734b909d0c3b4fcab40931059 Mon Sep 17 00:00:00 2001 From: Abyss Watcher Date: Tue, 15 Apr 2025 23:05:00 +0200 Subject: [PATCH 047/172] black --- volatility3/framework/layers/qemu.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/volatility3/framework/layers/qemu.py b/volatility3/framework/layers/qemu.py index 0ab359559..5190027c4 100644 --- a/volatility3/framework/layers/qemu.py +++ b/volatility3/framework/layers/qemu.py @@ -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") From ff5d736f94071aa3de9677f124ffb9fe79bcd367 Mon Sep 17 00:00:00 2001 From: j-t-1 <120829237+j-t-1@users.noreply.github.com> Date: Thu, 17 Apr 2025 20:35:38 +0100 Subject: [PATCH 048/172] Amend SvcDiff comments at top of file Previously contained information about Skeleton_Key_Check, change this to be about SvcDiff. --- volatility3/framework/plugins/windows/svcdiff.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/volatility3/framework/plugins/windows/svcdiff.py b/volatility3/framework/plugins/windows/svcdiff.py index d2c3da3d3..78b61eb67 100644 --- a/volatility3/framework/plugins/windows/svcdiff.py +++ b/volatility3/framework/plugins/windows/svcdiff.py @@ -1,15 +1,13 @@ # 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 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) +# This module compares services found through list walking versus scanning, +# with the aim of finding hidden services. # -# For a thorough walkthrough on how the R&D was performed to develop this plugin, -# please see our blogpost here: +# For background of hidden services and a real-world example of the use of this plugin, +# please see our blogpost: # -# https://volatility-labs.blogspot.com/2021/10/memory-forensics-r-illustrated.html +# https://volatilityfoundation.org/memory-forensics-rd-illustrated-detecting-hidden-windows-services/ import logging From 2641e5fc41b986b63f3772c90d79765c0b812574 Mon Sep 17 00:00:00 2001 From: David McDonald Date: Mon, 21 Apr 2025 15:50:57 -0500 Subject: [PATCH 049/172] ShimcacheMem: Fix exc getting module name `module.BaseDll.String` can raise an `InvalidAddressException`, this catches it and continues through the loop. --- .../framework/plugins/windows/shimcachemem.py | 20 ++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/volatility3/framework/plugins/windows/shimcachemem.py b/volatility3/framework/plugins/windows/shimcachemem.py index f60e5c5cf..5c0af7766 100644 --- a/volatility3/framework/plugins/windows/shimcachemem.py +++ b/volatility3/framework/plugins/windows/shimcachemem.py @@ -582,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] From bb91d688cb03db5afdf1cb72d76a972473896f05 Mon Sep 17 00:00:00 2001 From: David McDonald Date: Wed, 23 Apr 2025 10:57:22 -0500 Subject: [PATCH 050/172] Windows Handles: Convert to classmethods A lot of functionality in this plugin class was using instance methods instead of classmethods; this refactors the plugin to use classmethods instead of instance methods for consistency with the rest of the framework, and bumps the plugin major version to 4.0.0. --- .../framework/plugins/windows/handles.py | 90 +++++++++++++------ 1 file changed, 62 insertions(+), 28 deletions(-) diff --git a/volatility3/framework/plugins/windows/handles.py b/volatility3/framework/plugins/windows/handles.py index 2f257772f..64043d51d 100644 --- a/volatility3/framework/plugins/windows/handles.py +++ b/volatility3/framework/plugins/windows/handles.py @@ -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: From bceae67d439e24422531ffc8a3ea151e9e22f911 Mon Sep 17 00:00:00 2001 From: David McDonald Date: Wed, 23 Apr 2025 11:18:38 -0500 Subject: [PATCH 051/172] Windows Handles: Update dependents This updates the plugins that depend on `Handles` with the correct required version number, as well as calls to the new handles classmethods where needed. --- .../framework/plugins/windows/callbacks.py | 2 +- .../framework/plugins/windows/dumpfiles.py | 27 ++++++++----------- .../framework/plugins/windows/poolscanner.py | 2 +- .../framework/plugins/windows/psxview.py | 23 +++++++--------- 4 files changed, 22 insertions(+), 32 deletions(-) diff --git a/volatility3/framework/plugins/windows/callbacks.py b/volatility3/framework/plugins/windows/callbacks.py index bcdd37869..b8c9fe751 100644 --- a/volatility3/framework/plugins/windows/callbacks.py +++ b/volatility3/framework/plugins/windows/callbacks.py @@ -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) ), ] diff --git a/volatility3/framework/plugins/windows/dumpfiles.py b/volatility3/framework/plugins/windows/dumpfiles.py index e23b8ec3a..d069bc9f1 100755 --- a/volatility3/framework/plugins/windows/dumpfiles.py +++ b/volatility3/framework/plugins/windows/dumpfiles.py @@ -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": diff --git a/volatility3/framework/plugins/windows/poolscanner.py b/volatility3/framework/plugins/windows/poolscanner.py index 7030b4c9a..157282ce1 100644 --- a/volatility3/framework/plugins/windows/poolscanner.py +++ b/volatility3/framework/plugins/windows/poolscanner.py @@ -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", diff --git a/volatility3/framework/plugins/windows/psxview.py b/volatility3/framework/plugins/windows/psxview.py index 142987c3e..51616a22c 100644 --- a/volatility3/framework/plugins/windows/psxview.py +++ b/volatility3/framework/plugins/windows/psxview.py @@ -9,12 +9,7 @@ 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.plugins.windows import handles, pslist, psscan, thrdscan vollog = logging.getLogger(__name__) @@ -58,7 +53,7 @@ We recommend using -r pretty if you are looking at this plugin's output in a ter name="thrdscan", component=thrdscan.ThrdScan, version=(2, 0, 0) ), requirements.VersionRequirement( - name="handles", component=handles.Handles, version=(3, 0, 0) + name="handles", component=handles.Handles, version=(4, 0, 0) ), requirements.BooleanRequirement( name="physical-offsets", @@ -144,15 +139,11 @@ We recommend using -r pretty if you are looking at this plugin's output in a ter ) -> 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( + 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"] ) @@ -164,7 +155,11 @@ We recommend using -r pretty if you are looking at this plugin's output in a ter try: ret += [ handle.Body.cast("_EPROCESS") - for handle in handles_plugin.handles(p.ObjectTable) + 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: From e25e23d8cf509054bfbe6d5fe3a193b76caeb58f Mon Sep 17 00:00:00 2001 From: Elad Levi <99.elad.levi@gmail.com> Date: Thu, 24 Apr 2025 20:46:19 +0300 Subject: [PATCH 052/172] Create etwpatch.py --- .../framework/plugins/windows/etwpatch.py | 142 ++++++++++++++++++ 1 file changed, 142 insertions(+) create mode 100644 volatility3/framework/plugins/windows/etwpatch.py diff --git a/volatility3/framework/plugins/windows/etwpatch.py b/volatility3/framework/plugins/windows/etwpatch.py new file mode 100644 index 000000000..800f94623 --- /dev/null +++ b/volatility3/framework/plugins/windows/etwpatch.py @@ -0,0 +1,142 @@ +# etwpatch.py +# Plugin name: windows.etwpatch +# Volatility 3 plugin to detect ETW patching via EtwEventWrite prologue + +import contextlib +import logging + +from volatility3.framework import 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, pe_symbols + +vollog = logging.getLogger(__name__) + +class EtwPatch(interfaces.plugins.PluginInterface): + """Detects ETW patching by examining the first opcode of EtwEventWrite in ntdll.dll.""" + + # Plugin metadata for auto-discovery + _version = (1, 0, 0) + _required_framework_version = (2, 26, 0) + + @classmethod + def get_requirements(cls): + return [ + requirements.ModuleRequirement( + name='kernel', + description='Windows kernel', + architectures=["Intel32", "Intel64"] + ), + requirements.VersionRequirement( + name="pslist", component=pslist.PsList, version=(3, 0, 0) + ), + requirements.VersionRequirement( + name="pe_symbols", component=pslist.PsList, version=(3, 0, 0) + ), + requirements.ListRequirement( + name='pid', + description='Filter on specific process IDs', + element_type=int, + optional=True + ) + ] + + def _generator(self): + pid_filter = self.config.get('pid', None) + + for proc in pslist.PsList.list_processes( + context=self.context, + kernel_module_name=self.config['kernel']): + + # If the user passed --pid, only process those IDs + if pid_filter and proc.UniqueProcessId not in pid_filter: + continue + + pid = int(proc.UniqueProcessId) + proc_name = proc.ImageFileName.cast( + "string", + max_length = proc.ImageFileName.vol.count, + errors = 'replace' + ) + + # Build a per-process memory layer + try: + proc_layer_name = proc.add_process_layer() + except Exception: + continue + + proc_layer = self.context.layers[proc_layer_name] + + # Find ntdll.dll module + for module in proc.load_order_modules(): + BaseDllName = FullDllName = renderers.UnreadableValue() + with contextlib.suppress(exceptions.InvalidAddressException): + BaseDllName = module.BaseDllName.get_string() + FullDllName = module.FullDllName.get_string() + + if BaseDllName != 'ntdll.dll': + continue + + base = module.DllBase + size = module.SizeOfImage + + pe_table_name = intermed.IntermediateSymbolTable.create( + self.context, self.config_path, "windows", "pe", class_types=pe.class_types + ) + + pe_obj = pe_symbols.PESymbols.get_pefile_obj( + self.context, pe_table_name, proc_layer_name, base + ) + + try: + pe_obj.parse_data_directories( + directories=[pefile.DIRECTORY_ENTRY["IMAGE_DIRECTORY_ENTRY_EXPORT"]] + ) + except Exception as e: + vollog.debug(f"Error parsing IMAGE_DIRECTORY_ENTRY_EXPORT with {e}") + continue + + if not hasattr(pe_obj, "DIRECTORY_ENTRY_EXPORT"): + return None + + for export in pe_obj.DIRECTORY_ENTRY_EXPORT.symbols: + if export.name not in [b"EtwEventWrite", b"EtwEventWriteFull", b"NtTraceEvent"]: + continue + + function_start = base + export.address + try: + with contextlib.suppress(exceptions.InvalidAddressException): + opcode = self.context.layers[proc_layer_name].read( + function_start, 1 + ).hex() + + # 0xC3 = RET, 0xE9 = JMP (common ETW patches) + if opcode in ('c3', 'e9'): + yield (0, ( + pid, + proc_name, + BaseDllName, + export.name.decode(), + format_hints.Hex(function_start), + opcode + )) + except Exception as e: + vollog.debug(f"Error parsing IMAGE_DIRECTORY_ENTRY_EXPORT with {e}") + continue + finally: + break + + def run(self): + return renderers.TreeGrid( + [ + ("PID", int), + ("Process", str), + ("DLL", str), + ("Function", str), + ("Offset", format_hints.Hex), + ("Opcode", str) + ], + self._generator() + ) From 6ac76b27ff1dd4e402f1d26a3d174e3e436eee68 Mon Sep 17 00:00:00 2001 From: Elad Levi <99.elad.levi@gmail.com> Date: Thu, 24 Apr 2025 20:47:08 +0300 Subject: [PATCH 053/172] Update etwpatch.py --- volatility3/framework/plugins/windows/etwpatch.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/volatility3/framework/plugins/windows/etwpatch.py b/volatility3/framework/plugins/windows/etwpatch.py index 800f94623..76d769731 100644 --- a/volatility3/framework/plugins/windows/etwpatch.py +++ b/volatility3/framework/plugins/windows/etwpatch.py @@ -1,7 +1,6 @@ -# etwpatch.py -# Plugin name: windows.etwpatch -# Volatility 3 plugin to detect ETW patching via EtwEventWrite prologue - +# 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 contextlib import logging From 31184afe858e37106dfd6364e7e622fe6088967e Mon Sep 17 00:00:00 2001 From: Elad Levi <99.elad.levi@gmail.com> Date: Thu, 24 Apr 2025 20:48:59 +0300 Subject: [PATCH 054/172] Update etwpatch.py --- volatility3/framework/plugins/windows/etwpatch.py | 1 + 1 file changed, 1 insertion(+) diff --git a/volatility3/framework/plugins/windows/etwpatch.py b/volatility3/framework/plugins/windows/etwpatch.py index 76d769731..656e8a8ac 100644 --- a/volatility3/framework/plugins/windows/etwpatch.py +++ b/volatility3/framework/plugins/windows/etwpatch.py @@ -3,6 +3,7 @@ # import contextlib import logging +import pefile from volatility3.framework import exceptions, interfaces, renderers from volatility3.framework.configuration import requirements From a167ddc04dc5a6fd2bf77157bf41699218d3140e Mon Sep 17 00:00:00 2001 From: Elad Levi <99.elad.levi@gmail.com> Date: Thu, 24 Apr 2025 20:51:02 +0300 Subject: [PATCH 055/172] Update etwpatch.py --- volatility3/framework/plugins/windows/etwpatch.py | 1 - 1 file changed, 1 deletion(-) diff --git a/volatility3/framework/plugins/windows/etwpatch.py b/volatility3/framework/plugins/windows/etwpatch.py index 656e8a8ac..281f85172 100644 --- a/volatility3/framework/plugins/windows/etwpatch.py +++ b/volatility3/framework/plugins/windows/etwpatch.py @@ -17,7 +17,6 @@ vollog = logging.getLogger(__name__) class EtwPatch(interfaces.plugins.PluginInterface): """Detects ETW patching by examining the first opcode of EtwEventWrite in ntdll.dll.""" - # Plugin metadata for auto-discovery _version = (1, 0, 0) _required_framework_version = (2, 26, 0) From b5c98a080a5a7823a69457af93436d2f3cdb169e Mon Sep 17 00:00:00 2001 From: Mike Auty Date: Fri, 25 Apr 2025 11:31:45 +0100 Subject: [PATCH 056/172] Fix a default value being mutable --- volatility3/cli/volshell/generic.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/volatility3/cli/volshell/generic.py b/volatility3/cli/volshell/generic.py index 2ea722a37..b9811e7d5 100644 --- a/volatility3/cli/volshell/generic.py +++ b/volatility3/cli/volshell/generic.py @@ -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: From 2d1acaca51ce70a1582e99d87a394aee8711418b Mon Sep 17 00:00:00 2001 From: eve Date: Fri, 25 Apr 2025 16:40:06 +0100 Subject: [PATCH 057/172] Linux: update extensions used by kallsyms plugin to use utility.pointer_to_string --- .../symbols/linux/extensions/__init__.py | 20 ++++--------------- 1 file changed, 4 insertions(+), 16 deletions(-) diff --git a/volatility3/framework/symbols/linux/extensions/__init__.py b/volatility3/framework/symbols/linux/extensions/__init__.py index d281e2b9b..bd57b726a 100644 --- a/volatility3/framework/symbols/linux/extensions/__init__.py +++ b/volatility3/framework/symbols/linux/extensions/__init__.py @@ -3065,14 +3065,7 @@ class kernel_symbol(objects.StructType): else: raise AttributeError("Unsupported kernel_symbol type implementation") - layer = self._context.layers[self.vol.layer_name] - name_bytes = layer.read(name_offset, linux_constants.KSYM_NAME_LEN) - - idx = name_bytes.find(b"\x00") - if idx != -1: - name_bytes = name_bytes[:idx] - - return name_bytes.decode("utf-8", errors="ignore") + return utility.pointer_to_string(name_offset, linux_constants.KSYM_NAME_LEN) def get_name(self) -> Optional[str]: try: @@ -3108,14 +3101,9 @@ class kernel_symbol(objects.StructType): else: raise AttributeError("Unsupported kernel_symbol type implementation") - layer = self._context.layers[self.vol.layer_name] - namespace_bytes = layer.read(namespace_offset, linux_constants.KSYM_NAME_LEN) - - idx = namespace_bytes.find(b"\x00") - if idx != -1: - namespace_bytes = namespace_bytes[:idx] - - return namespace_bytes.decode("utf-8", errors="ignore") + return utility.pointer_to_string( + namespace_offset, linux_constants.KSYM_NAME_LEN + ) def get_namespace(self) -> Optional[str]: try: From 2d80f4680e41c373872386f7feef628e2ce8b7a4 Mon Sep 17 00:00:00 2001 From: eve Date: Fri, 25 Apr 2025 16:52:48 +0100 Subject: [PATCH 058/172] Linux: update kernel_symbol extensions _do_get_name and _do_get_namespace to use errors='ignore' to match previous implimentation --- volatility3/framework/symbols/linux/extensions/__init__.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/volatility3/framework/symbols/linux/extensions/__init__.py b/volatility3/framework/symbols/linux/extensions/__init__.py index bd57b726a..c980ee6b1 100644 --- a/volatility3/framework/symbols/linux/extensions/__init__.py +++ b/volatility3/framework/symbols/linux/extensions/__init__.py @@ -3065,7 +3065,9 @@ class kernel_symbol(objects.StructType): else: raise AttributeError("Unsupported kernel_symbol type implementation") - return utility.pointer_to_string(name_offset, linux_constants.KSYM_NAME_LEN) + return utility.pointer_to_string( + name_offset, linux_constants.KSYM_NAME_LEN, errors="ignore" + ) def get_name(self) -> Optional[str]: try: @@ -3102,7 +3104,7 @@ class kernel_symbol(objects.StructType): raise AttributeError("Unsupported kernel_symbol type implementation") return utility.pointer_to_string( - namespace_offset, linux_constants.KSYM_NAME_LEN + namespace_offset, linux_constants.KSYM_NAME_LEN, errors="ignore" ) def get_namespace(self) -> Optional[str]: From 9c7655a63586d5dce32989fac7c5ea15261e27e4 Mon Sep 17 00:00:00 2001 From: Mike Auty Date: Sun, 27 Apr 2025 14:56:17 +0100 Subject: [PATCH 059/172] Add in coverage recording to the test process --- .github/workflows/test.yaml | 101 +++++++++++++++++++----------------- .gitignore | 3 ++ pyproject.toml | 1 + 3 files changed, 58 insertions(+), 47 deletions(-) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 56d671102..266e8bcc3 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -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 - diff --git a/.gitignore b/.gitignore index c132736a9..c550db705 100644 --- a/.gitignore +++ b/.gitignore @@ -43,3 +43,6 @@ ENV/ # PyTest cache files .pytest_cache/ + +# Coverage cache +.coverage diff --git a/pyproject.toml b/pyproject.toml index abd2e79f7..a70f2c633 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -45,6 +45,7 @@ dev = [ test = [ "volatility3[dev]", "pytest>=8.3.3,<9", + "pytest-cov>=6.1.1,<7", "yara-x>=0.10.0,<1", ] From bc265edcdc5b6f07dad5e139bed647f1cf070458 Mon Sep 17 00:00:00 2001 From: Mike Auty Date: Sun, 27 Apr 2025 15:00:03 +0100 Subject: [PATCH 060/172] Try to slightly loosen the pytest-cov requirement --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index a70f2c633..006f92a08 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -45,7 +45,7 @@ dev = [ test = [ "volatility3[dev]", "pytest>=8.3.3,<9", - "pytest-cov>=6.1.1,<7", + "pytest-cov>=6,<7", "yara-x>=0.10.0,<1", ] From f4a2c7aec23b3c613e7af6fe59b528ee57c78517 Mon Sep 17 00:00:00 2001 From: Mike Auty Date: Sun, 27 Apr 2025 15:02:12 +0100 Subject: [PATCH 061/172] Drop pycov requirement by 1 since github can't seem to find the most recent version --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 006f92a08..b88ac7752 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -45,7 +45,7 @@ dev = [ test = [ "volatility3[dev]", "pytest>=8.3.3,<9", - "pytest-cov>=6,<7", + "pytest-cov>=5,<7", "yara-x>=0.10.0,<1", ] From d3da34ce10b6c0dbf56ad54b50498a43a4fcfeef Mon Sep 17 00:00:00 2001 From: Mike Auty Date: Sun, 27 Apr 2025 16:18:47 +0100 Subject: [PATCH 062/172] Update codeql action from v2 to v3 --- .github/workflows/codeql.yml | 61 ++++++++++++++++++------------------ 1 file changed, 30 insertions(+), 31 deletions(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index e2e741a9f..324390e43 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -13,10 +13,10 @@ name: "CodeQL" on: push: - branches: [ "develop" ] + branches: ["develop"] pull_request: # The branches below must be a subset of the branches above - branches: [ "develop" ] + branches: ["develop"] # schedule: # - cron: '16 8 * * 0' @@ -32,43 +32,42 @@ jobs: strategy: fail-fast: false matrix: - language: [ 'python' ] + language: ["python"] # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support steps: - - name: Checkout repository - uses: actions/checkout@v3 + - name: Checkout repository + uses: actions/checkout@v3 - # 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}}" From 41142ef621cff7550bb14fba758349325bc21ddc Mon Sep 17 00:00:00 2001 From: eve Date: Mon, 28 Apr 2025 07:18:47 +0100 Subject: [PATCH 063/172] cli: add debug log of plugin version when the plugin is successfully constructed --- volatility3/cli/__init__.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/volatility3/cli/__init__.py b/volatility3/cli/__init__.py index a41cf95a3..cc97c4fcb 100644 --- a/volatility3/cli/__init__.py +++ b/volatility3/cli/__init__.py @@ -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) From 533f96ec03dd800f0c664b9c627999b2b07c2d74 Mon Sep 17 00:00:00 2001 From: Elad Levi <99.elad.levi@gmail.com> Date: Mon, 28 Apr 2025 12:53:46 +0000 Subject: [PATCH 064/172] Added extended usage of pe_symbols --- .../framework/plugins/windows/etwpatch.py | 114 ++++++++---------- 1 file changed, 52 insertions(+), 62 deletions(-) diff --git a/volatility3/framework/plugins/windows/etwpatch.py b/volatility3/framework/plugins/windows/etwpatch.py index 281f85172..36cb4b9a9 100644 --- a/volatility3/framework/plugins/windows/etwpatch.py +++ b/volatility3/framework/plugins/windows/etwpatch.py @@ -42,90 +42,80 @@ class EtwPatch(interfaces.plugins.PluginInterface): ) ] + def _get_dll_vads(self, proc, dll_name): + """Retrieve VADs for a specific DLL in the process.""" + collected_modules = pe_symbols.PESymbols.get_proc_vads_with_file_paths(proc) + return [ + (vad_start, vad_size, proc.add_process_layer()) + for vad_start, vad_size, filepath in collected_modules + if pe_symbols.PESymbols.filename_for_path(filepath) == dll_name + ] + + def _find_symbols(self, dll_name, symbols, proc_layer_name, dll_vads): + """Find symbols for a specific DLL.""" + filter_module = {dll_name: {"names": symbols}} + process_modules = {dll_name: [(proc_layer_name, vad_start, vad_size) for vad_start, vad_size, _ in dll_vads]} + return pe_symbols.PESymbols.find_symbols(self.context, self.config_path, filter_module, process_modules) + + def _get_first_opcode(self, proc_layer_name, function_start): + """Check the first opcode of a function.""" + try: + return self.context.layers[proc_layer_name].read(function_start, 1).hex() + except exceptions.InvalidAddressException: + return None + def _generator(self): pid_filter = self.config.get('pid', None) for proc in pslist.PsList.list_processes( context=self.context, kernel_module_name=self.config['kernel']): - - # If the user passed --pid, only process those IDs + # Skip processes not in the PID filter if pid_filter and proc.UniqueProcessId not in pid_filter: continue pid = int(proc.UniqueProcessId) proc_name = proc.ImageFileName.cast( "string", - max_length = proc.ImageFileName.vol.count, - errors = 'replace' + max_length=proc.ImageFileName.vol.count, + errors='replace' ) - # Build a per-process memory layer try: proc_layer_name = proc.add_process_layer() - except Exception: + except exceptions.InvalidAddressException: continue - proc_layer = self.context.layers[proc_layer_name] + dlls_to_check = { + "ntdll.dll": [ + "EtwEventWrite", + "EtwEventWriteFull", + "NtTraceEvent" + ], + "advapi32.dll": [ + "EventWrite" + ] + } - # Find ntdll.dll module - for module in proc.load_order_modules(): - BaseDllName = FullDllName = renderers.UnreadableValue() - with contextlib.suppress(exceptions.InvalidAddressException): - BaseDllName = module.BaseDllName.get_string() - FullDllName = module.FullDllName.get_string() - - if BaseDllName != 'ntdll.dll': + for dll_name, symbols in dlls_to_check.items(): + dll_vads = self._get_dll_vads(proc, dll_name) + if not dll_vads: continue - base = module.DllBase - size = module.SizeOfImage - - pe_table_name = intermed.IntermediateSymbolTable.create( - self.context, self.config_path, "windows", "pe", class_types=pe.class_types - ) - - pe_obj = pe_symbols.PESymbols.get_pefile_obj( - self.context, pe_table_name, proc_layer_name, base - ) - - try: - pe_obj.parse_data_directories( - directories=[pefile.DIRECTORY_ENTRY["IMAGE_DIRECTORY_ENTRY_EXPORT"]] - ) - except Exception as e: - vollog.debug(f"Error parsing IMAGE_DIRECTORY_ENTRY_EXPORT with {e}") + found_symbols, _ = self._find_symbols(dll_name, symbols, proc_layer_name, dll_vads) + if dll_name not in found_symbols: continue - - if not hasattr(pe_obj, "DIRECTORY_ENTRY_EXPORT"): - return None - - for export in pe_obj.DIRECTORY_ENTRY_EXPORT.symbols: - if export.name not in [b"EtwEventWrite", b"EtwEventWriteFull", b"NtTraceEvent"]: - continue - - function_start = base + export.address - try: - with contextlib.suppress(exceptions.InvalidAddressException): - opcode = self.context.layers[proc_layer_name].read( - function_start, 1 - ).hex() - - # 0xC3 = RET, 0xE9 = JMP (common ETW patches) - if opcode in ('c3', 'e9'): - yield (0, ( - pid, - proc_name, - BaseDllName, - export.name.decode(), - format_hints.Hex(function_start), - opcode - )) - except Exception as e: - vollog.debug(f"Error parsing IMAGE_DIRECTORY_ENTRY_EXPORT with {e}") - continue - finally: - break + + for symbol_name, function_start in found_symbols[dll_name]: + opcode = self._get_first_opcode(proc_layer_name, function_start) + if opcode in ('c3', 'e9'): # RET or JMP + yield (0, ( + pid, + proc_name, + dll_name, + symbol_name, + format_hints.Hex(function_start), opcode + )) def run(self): return renderers.TreeGrid( From 02634d0e30edd2cabfb966c54595b892f245194c Mon Sep 17 00:00:00 2001 From: Elad Levi <99.elad.levi@gmail.com> Date: Mon, 28 Apr 2025 12:54:46 +0000 Subject: [PATCH 065/172] oops --- volatility3/framework/plugins/windows/etwpatch.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/volatility3/framework/plugins/windows/etwpatch.py b/volatility3/framework/plugins/windows/etwpatch.py index 36cb4b9a9..316af4dab 100644 --- a/volatility3/framework/plugins/windows/etwpatch.py +++ b/volatility3/framework/plugins/windows/etwpatch.py @@ -114,7 +114,8 @@ class EtwPatch(interfaces.plugins.PluginInterface): proc_name, dll_name, symbol_name, - format_hints.Hex(function_start), opcode + format_hints.Hex(function_start), + opcode )) def run(self): From d449104d8ba1e1459a58fdd6cdf4a6070703a723 Mon Sep 17 00:00:00 2001 From: Elad Levi <99.elad.levi@gmail.com> Date: Mon, 28 Apr 2025 13:26:14 +0000 Subject: [PATCH 066/172] removed unnecessary imports --- volatility3/framework/plugins/windows/etwpatch.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/volatility3/framework/plugins/windows/etwpatch.py b/volatility3/framework/plugins/windows/etwpatch.py index 316af4dab..d6632617a 100644 --- a/volatility3/framework/plugins/windows/etwpatch.py +++ b/volatility3/framework/plugins/windows/etwpatch.py @@ -1,15 +1,11 @@ # 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 contextlib import logging -import pefile from volatility3.framework import 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, pe_symbols vollog = logging.getLogger(__name__) From c9386782724311f2b1f8c373dc4af414dc4f875f Mon Sep 17 00:00:00 2001 From: Elad Levi <99.elad.levi@gmail.com> Date: Mon, 28 Apr 2025 14:21:24 +0000 Subject: [PATCH 067/172] Fixed according to atcuno comments --- .../framework/plugins/windows/etwpatch.py | 122 ++++++++---------- 1 file changed, 55 insertions(+), 67 deletions(-) diff --git a/volatility3/framework/plugins/windows/etwpatch.py b/volatility3/framework/plugins/windows/etwpatch.py index d6632617a..79ddf2e1a 100644 --- a/volatility3/framework/plugins/windows/etwpatch.py +++ b/volatility3/framework/plugins/windows/etwpatch.py @@ -1,10 +1,11 @@ -# 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 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 @@ -16,6 +17,11 @@ class EtwPatch(interfaces.plugins.PluginInterface): _version = (1, 0, 0) _required_framework_version = (2, 26, 0) + etw_functions = { + "ntdll.dll": ["EtwEventWrite", "EtwEventWriteFull", "NtTraceEvent"], + "advapi32.dll": ["EventWrite"], + } + @classmethod def get_requirements(cls): return [ @@ -38,81 +44,63 @@ class EtwPatch(interfaces.plugins.PluginInterface): ) ] - def _get_dll_vads(self, proc, dll_name): - """Retrieve VADs for a specific DLL in the process.""" - collected_modules = pe_symbols.PESymbols.get_proc_vads_with_file_paths(proc) - return [ - (vad_start, vad_size, proc.add_process_layer()) - for vad_start, vad_size, filepath in collected_modules - if pe_symbols.PESymbols.filename_for_path(filepath) == dll_name - ] - - def _find_symbols(self, dll_name, symbols, proc_layer_name, dll_vads): - """Find symbols for a specific DLL.""" - filter_module = {dll_name: {"names": symbols}} - process_modules = {dll_name: [(proc_layer_name, vad_start, vad_size) for vad_start, vad_size, _ in dll_vads]} - return pe_symbols.PESymbols.find_symbols(self.context, self.config_path, filter_module, process_modules) - - def _get_first_opcode(self, proc_layer_name, function_start): - """Check the first opcode of a function.""" - try: - return self.context.layers[proc_layer_name].read(function_start, 1).hex() - except exceptions.InvalidAddressException: - return None - def _generator(self): - pid_filter = self.config.get('pid', None) + # 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']): - # Skip processes not in the PID filter - if pid_filter and proc.UniqueProcessId not in pid_filter: - continue - - pid = int(proc.UniqueProcessId) - proc_name = proc.ImageFileName.cast( - "string", - max_length=proc.ImageFileName.vol.count, - errors='replace' - ) - + 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 - dlls_to_check = { - "ntdll.dll": [ - "EtwEventWrite", - "EtwEventWriteFull", - "NtTraceEvent" - ], - "advapi32.dll": [ - "EventWrite" - ] + # Map of opcodes to their instruction names + opcode_map = { + 'c3': 'RET', + 'e9': 'JMP', } - for dll_name, symbols in dlls_to_check.items(): - dll_vads = self._get_dll_vads(proc, dll_name) - if not dll_vads: - continue - - found_symbols, _ = self._find_symbols(dll_name, symbols, proc_layer_name, dll_vads) - if dll_name not in found_symbols: - continue - - for symbol_name, function_start in found_symbols[dll_name]: - opcode = self._get_first_opcode(proc_layer_name, function_start) - if opcode in ('c3', 'e9'): # RET or JMP - yield (0, ( - pid, - proc_name, - dll_name, - symbol_name, - format_hints.Hex(function_start), - opcode - )) + 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 + ).hex() + 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} ({instruction})" + ), + ) + except exceptions.InvalidAddressException: + vollog.debug(f"Invalid address when reading function {func_name} at {func_addr:#x} in process {proc_id}") + continue + except KeyError: + # Layer may no longer exist + vollog.debug(f"Layer {proc_layer_name} no longer exists for process {proc_id}") + continue def run(self): return renderers.TreeGrid( @@ -122,7 +110,7 @@ class EtwPatch(interfaces.plugins.PluginInterface): ("DLL", str), ("Function", str), ("Offset", format_hints.Hex), - ("Opcode", str) + ("Opcode", str), ], - self._generator() + self._generator(), ) From 817bd5ce7fb26984cfb0ab5860d00c24fd3bc3f8 Mon Sep 17 00:00:00 2001 From: Elad Levi <99.elad.levi@gmail.com> Date: Mon, 28 Apr 2025 15:07:03 +0000 Subject: [PATCH 068/172] additional fixes --- .../framework/plugins/windows/etwpatch.py | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/volatility3/framework/plugins/windows/etwpatch.py b/volatility3/framework/plugins/windows/etwpatch.py index 79ddf2e1a..1ecedea3f 100644 --- a/volatility3/framework/plugins/windows/etwpatch.py +++ b/volatility3/framework/plugins/windows/etwpatch.py @@ -18,8 +18,18 @@ class EtwPatch(interfaces.plugins.PluginInterface): _required_framework_version = (2, 26, 0) etw_functions = { - "ntdll.dll": ["EtwEventWrite", "EtwEventWriteFull", "NtTraceEvent"], - "advapi32.dll": ["EventWrite"], + "ntdll.dll": { + pe_symbols.wanted_names_identifier: [ + "EtwEventWrite", + "EtwEventWriteFull", + "NtTraceEvent" + ], + }, + "advapi32.dll": { + pe_symbols.wanted_names_identifier:[ + "EventWrite" + ], + }, } @classmethod @@ -96,11 +106,6 @@ class EtwPatch(interfaces.plugins.PluginInterface): ) except exceptions.InvalidAddressException: vollog.debug(f"Invalid address when reading function {func_name} at {func_addr:#x} in process {proc_id}") - continue - except KeyError: - # Layer may no longer exist - vollog.debug(f"Layer {proc_layer_name} no longer exists for process {proc_id}") - continue def run(self): return renderers.TreeGrid( From c5a4b34bfa5f1466b61551768ea0ad61becbc200 Mon Sep 17 00:00:00 2001 From: Elad Levi <99.elad.levi@gmail.com> Date: Mon, 28 Apr 2025 15:18:53 +0000 Subject: [PATCH 069/172] Fixed plugin docs --- volatility3/framework/plugins/windows/etwpatch.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/volatility3/framework/plugins/windows/etwpatch.py b/volatility3/framework/plugins/windows/etwpatch.py index 1ecedea3f..3605dfccb 100644 --- a/volatility3/framework/plugins/windows/etwpatch.py +++ b/volatility3/framework/plugins/windows/etwpatch.py @@ -12,7 +12,13 @@ from volatility3.plugins.windows import pslist, pe_symbols vollog = logging.getLogger(__name__) class EtwPatch(interfaces.plugins.PluginInterface): - """Detects ETW patching by examining the first opcode of EtwEventWrite in ntdll.dll.""" + """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) From 281a237e03296ff38c7d0353157fa068c32a8c39 Mon Sep 17 00:00:00 2001 From: Elad Levi <99.elad.levi@gmail.com> Date: Mon, 28 Apr 2025 16:03:45 +0000 Subject: [PATCH 070/172] black and ruff fixes --- .../framework/plugins/windows/etwpatch.py | 63 ++++++++++--------- 1 file changed, 33 insertions(+), 30 deletions(-) diff --git a/volatility3/framework/plugins/windows/etwpatch.py b/volatility3/framework/plugins/windows/etwpatch.py index 3605dfccb..e79735213 100644 --- a/volatility3/framework/plugins/windows/etwpatch.py +++ b/volatility3/framework/plugins/windows/etwpatch.py @@ -7,16 +7,17 @@ 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 +from volatility3.plugins.windows import pslist, pe_symbols vollog = logging.getLogger(__name__) + 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 + + 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. """ @@ -28,13 +29,11 @@ class EtwPatch(interfaces.plugins.PluginInterface): pe_symbols.wanted_names_identifier: [ "EtwEventWrite", "EtwEventWriteFull", - "NtTraceEvent" + "NtTraceEvent", ], }, "advapi32.dll": { - pe_symbols.wanted_names_identifier:[ - "EventWrite" - ], + pe_symbols.wanted_names_identifier: ["EventWrite"], }, } @@ -42,22 +41,22 @@ class EtwPatch(interfaces.plugins.PluginInterface): def get_requirements(cls): return [ requirements.ModuleRequirement( - name='kernel', - description='Windows kernel', - architectures=["Intel32", "Intel64"] + 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=pslist.PsList, version=(3, 0, 0) + name="pe_symbols", component=pe_symbols.PESymbols, version=(3, 0, 0) ), requirements.ListRequirement( - name='pid', - description='Filter on specific process IDs', + name="pid", + description="Filter on specific process IDs", element_type=int, - optional=True - ) + optional=True, + ), ] def _generator(self): @@ -68,15 +67,15 @@ class EtwPatch(interfaces.plugins.PluginInterface): 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, - ): - + 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) @@ -87,16 +86,18 @@ class EtwPatch(interfaces.plugins.PluginInterface): # Map of opcodes to their instruction names opcode_map = { - 'c3': 'RET', - 'e9': 'JMP', + "c3": "RET", + "e9": "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 - ).hex() + opcode = ( + self.context.layers[proc_layer_name] + .read(func_addr, 1) + .hex() + ) if opcode in opcode_map: instruction = opcode_map[opcode] yield ( @@ -107,11 +108,13 @@ class EtwPatch(interfaces.plugins.PluginInterface): dll_name, func_name, format_hints.Hex(func_addr), - f"{opcode} ({instruction})" + f"{opcode} ({instruction})", ), ) except exceptions.InvalidAddressException: - vollog.debug(f"Invalid address when reading function {func_name} at {func_addr:#x} in process {proc_id}") + vollog.debug( + f"Invalid address when reading function {func_name} at {func_addr:#x} in process {proc_id}" + ) def run(self): return renderers.TreeGrid( From 39b35a3a8070854fa3a9238a1190e5978288df3f Mon Sep 17 00:00:00 2001 From: Elad Levi <99.elad.levi@gmail.com> Date: Tue, 29 Apr 2025 13:11:35 +0300 Subject: [PATCH 071/172] Update volatility3/framework/plugins/windows/etwpatch.py Co-authored-by: ikelos --- volatility3/framework/plugins/windows/etwpatch.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/volatility3/framework/plugins/windows/etwpatch.py b/volatility3/framework/plugins/windows/etwpatch.py index e79735213..7aee06e43 100644 --- a/volatility3/framework/plugins/windows/etwpatch.py +++ b/volatility3/framework/plugins/windows/etwpatch.py @@ -86,8 +86,8 @@ class EtwPatch(interfaces.plugins.PluginInterface): # Map of opcodes to their instruction names opcode_map = { - "c3": "RET", - "e9": "JMP", + 0xc3: "RET", + 0xe9: "JMP", } for dll_name, functions in found_symbols.items(): From cc1df7b4617be694ec22aac3ca49d7c5175058d1 Mon Sep 17 00:00:00 2001 From: Elad Levi <99.elad.levi@gmail.com> Date: Tue, 29 Apr 2025 13:11:47 +0300 Subject: [PATCH 072/172] Update volatility3/framework/plugins/windows/etwpatch.py Co-authored-by: ikelos --- volatility3/framework/plugins/windows/etwpatch.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/volatility3/framework/plugins/windows/etwpatch.py b/volatility3/framework/plugins/windows/etwpatch.py index 7aee06e43..30e8b2947 100644 --- a/volatility3/framework/plugins/windows/etwpatch.py +++ b/volatility3/framework/plugins/windows/etwpatch.py @@ -95,8 +95,7 @@ class EtwPatch(interfaces.plugins.PluginInterface): try: opcode = ( self.context.layers[proc_layer_name] - .read(func_addr, 1) - .hex() + .read(func_addr, 1)[0] ) if opcode in opcode_map: instruction = opcode_map[opcode] From 4738efefa3cdc4a4405df57551ffcc653bff9b9e Mon Sep 17 00:00:00 2001 From: Elad Levi <99.elad.levi@gmail.com> Date: Tue, 29 Apr 2025 13:11:58 +0300 Subject: [PATCH 073/172] Update volatility3/framework/plugins/windows/etwpatch.py Co-authored-by: ikelos --- volatility3/framework/plugins/windows/etwpatch.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/volatility3/framework/plugins/windows/etwpatch.py b/volatility3/framework/plugins/windows/etwpatch.py index 30e8b2947..a21cab030 100644 --- a/volatility3/framework/plugins/windows/etwpatch.py +++ b/volatility3/framework/plugins/windows/etwpatch.py @@ -107,7 +107,7 @@ class EtwPatch(interfaces.plugins.PluginInterface): dll_name, func_name, format_hints.Hex(func_addr), - f"{opcode} ({instruction})", + f"{opcode:02x} ({instruction})", ), ) except exceptions.InvalidAddressException: From 19b094acf686f3a0a093a1ac6b62b2b0d1391c5f Mon Sep 17 00:00:00 2001 From: Elad Levi <99.elad.levi@gmail.com> Date: Tue, 29 Apr 2025 14:20:42 +0300 Subject: [PATCH 074/172] Fix lint errors --- volatility3/framework/plugins/windows/etwpatch.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/volatility3/framework/plugins/windows/etwpatch.py b/volatility3/framework/plugins/windows/etwpatch.py index a21cab030..14fbacc28 100644 --- a/volatility3/framework/plugins/windows/etwpatch.py +++ b/volatility3/framework/plugins/windows/etwpatch.py @@ -86,17 +86,16 @@ class EtwPatch(interfaces.plugins.PluginInterface): # Map of opcodes to their instruction names opcode_map = { - 0xc3: "RET", - 0xe9: "JMP", + 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] - ) + opcode = self.context.layers[proc_layer_name].read( + func_addr, 1 + )[0] if opcode in opcode_map: instruction = opcode_map[opcode] yield ( From f5ec6dc7dd5fbf4785007365fd89e1d09304afd5 Mon Sep 17 00:00:00 2001 From: Mike Auty Date: Mon, 5 May 2025 12:55:48 +0100 Subject: [PATCH 075/172] Add in initial version of breakpointing --- volatility3/cli/volshell/generic.py | 32 +++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/volatility3/cli/volshell/generic.py b/volatility3/cli/volshell/generic.py index b9811e7d5..7ff30909f 100644 --- a/volatility3/cli/volshell/generic.py +++ b/volatility3/cli/volshell/generic.py @@ -3,6 +3,7 @@ # import binascii import code +import functools import io import random import string @@ -187,6 +188,7 @@ 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 [ + (["bp", "breakpoint"], self.breakpoint), (["dt", "display_type"], self.display_type), (["db", "display_bytes"], self.display_bytes), (["dw", "display_words"], self.display_words), @@ -797,6 +799,36 @@ class Volshell(interfaces.plugins.PluginInterface): return constructed + def breakpoint(self, address: int, layer_name: Optional[str] = None) -> 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] + # 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): + import pdb + + pdb.set_trace() + print("Hit breakpoint") + 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 + breakpoints = getattr(layer.read, "breakpoints") + breakpoints.add(address) + setattr(layer.read, "breakpoints", breakpoints) + class NullFileHandler(io.BytesIO, interfaces.plugins.FileHandlerInterface): """Null FileHandler that swallows files whole without consuming memory""" From ae08c8ecfc8dd85f3c33af3a4307dd1cd86df243 Mon Sep 17 00:00:00 2001 From: Mike Auty Date: Mon, 5 May 2025 13:14:19 +0100 Subject: [PATCH 076/172] Support setting the breakpoint on the lowest layer --- volatility3/cli/volshell/generic.py | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/volatility3/cli/volshell/generic.py b/volatility3/cli/volshell/generic.py index 7ff30909f..20fbae417 100644 --- a/volatility3/cli/volshell/generic.py +++ b/volatility3/cli/volshell/generic.py @@ -799,7 +799,9 @@ class Volshell(interfaces.plugins.PluginInterface): return constructed - def breakpoint(self, address: int, layer_name: Optional[str] = None) -> None: + 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: @@ -807,6 +809,18 @@ class Volshell(interfaces.plugins.PluginInterface): 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 @@ -825,6 +839,7 @@ class Volshell(interfaces.plugins.PluginInterface): 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) From 5bb4cd90c16da4b2248a97ad80a04f0b852ecf51 Mon Sep 17 00:00:00 2001 From: Mike Auty Date: Mon, 5 May 2025 13:16:29 +0100 Subject: [PATCH 077/172] Remove no longer needed functools import --- volatility3/cli/volshell/generic.py | 1 - 1 file changed, 1 deletion(-) diff --git a/volatility3/cli/volshell/generic.py b/volatility3/cli/volshell/generic.py index 20fbae417..4c90c39c9 100644 --- a/volatility3/cli/volshell/generic.py +++ b/volatility3/cli/volshell/generic.py @@ -3,7 +3,6 @@ # import binascii import code -import functools import io import random import string From 140a16d1dd263e4985413d166fb6e40111fa1051 Mon Sep 17 00:00:00 2001 From: Abyss Watcher Date: Mon, 5 May 2025 21:44:05 +0200 Subject: [PATCH 078/172] prevent forward slashes only dentry names --- volatility3/framework/symbols/linux/__init__.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/volatility3/framework/symbols/linux/__init__.py b/volatility3/framework/symbols/linux/__init__.py index 9d3930087..fbe0fdad3 100644 --- a/volatility3/framework/symbols/linux/__init__.py +++ b/volatility3/framework/symbols/linux/__init__.py @@ -190,9 +190,13 @@ class LinuxUtilities(interfaces.configuration.VersionableInterface): parent = dentry.d_parent dname = dentry.d_name.name_as_str() - path_reversed.append(dname.strip("/")) + dname_stripped = dname.strip("/") + if dname_stripped: + path_reversed.append(dname_stripped) dentry = parent + if path_reversed == []: + return "" path = "/" + "/".join(reversed(path_reversed)) return path From 2759df89dbf38ba3bd515624642db527b16b03e6 Mon Sep 17 00:00:00 2001 From: Abyss Watcher Date: Tue, 6 May 2025 00:31:09 +0200 Subject: [PATCH 079/172] tag potentially smeared dentry names --- volatility3/framework/symbols/linux/__init__.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/volatility3/framework/symbols/linux/__init__.py b/volatility3/framework/symbols/linux/__init__.py index fbe0fdad3..65f20d88d 100644 --- a/volatility3/framework/symbols/linux/__init__.py +++ b/volatility3/framework/symbols/linux/__init__.py @@ -169,6 +169,7 @@ class LinuxUtilities(interfaces.configuration.VersionableInterface): return "" path_reversed = [] + smeared = False while ( dentry and dentry.is_readable() @@ -190,14 +191,16 @@ class LinuxUtilities(interfaces.configuration.VersionableInterface): parent = dentry.d_parent dname = dentry.d_name.name_as_str() - dname_stripped = dname.strip("/") - if dname_stripped: - path_reversed.append(dname_stripped) + # empty dentry names are most likely + # the result of smearing + if not dname: + smeared = True + path_reversed.append(dname.strip("/")) dentry = parent - if path_reversed == []: - return "" path = "/" + "/".join(reversed(path_reversed)) + if smeared: + return f" {path}" return path @classmethod From 92673be42c761051561c3894050b6a0fb06c17f1 Mon Sep 17 00:00:00 2001 From: Mike Auty Date: Tue, 6 May 2025 22:15:38 +0100 Subject: [PATCH 080/172] Reformat and install volatility package deps before build --- .github/workflows/build-pyinstaller.yml | 60 ++++++++++++------------- 1 file changed, 30 insertions(+), 30 deletions(-) diff --git a/.github/workflows/build-pyinstaller.yml b/.github/workflows/build-pyinstaller.yml index bcba95403..21c2d8acf 100644 --- a/.github/workflows/build-pyinstaller.yml +++ b/.github/workflows/build-pyinstaller.yml @@ -4,47 +4,47 @@ on: branches: - stable - develop - - 'release/**' + - "release/**" pull_request: branches: - stable - - 'release/**' + - "release/**" 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@v3 + - 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 From ee445d974692f3061dd6cb7eaae7c653381c740f Mon Sep 17 00:00:00 2001 From: Mike Auty Date: Tue, 6 May 2025 22:27:03 +0100 Subject: [PATCH 081/172] Add in workflow_dispatch for pyinstaller action --- .github/workflows/build-pyinstaller.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/build-pyinstaller.yml b/.github/workflows/build-pyinstaller.yml index 21c2d8acf..b1d15eb10 100644 --- a/.github/workflows/build-pyinstaller.yml +++ b/.github/workflows/build-pyinstaller.yml @@ -9,6 +9,7 @@ on: branches: - stable - "release/**" + workflow_dispatch: jobs: exe: From 718f8ca2b583d8fa8626502566f31fe1d5b92b3e Mon Sep 17 00:00:00 2001 From: ikelos Date: Tue, 6 May 2025 22:41:21 +0100 Subject: [PATCH 082/172] Update volatility3/framework/plugins/linux/vmayarascan.py --- volatility3/framework/plugins/linux/vmayarascan.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/volatility3/framework/plugins/linux/vmayarascan.py b/volatility3/framework/plugins/linux/vmayarascan.py index 9a413dd9d..64f5827c1 100644 --- a/volatility3/framework/plugins/linux/vmayarascan.py +++ b/volatility3/framework/plugins/linux/vmayarascan.py @@ -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", From c0c7119c38e18ed92349d2ee7affe837bcba3aa0 Mon Sep 17 00:00:00 2001 From: ikelos Date: Tue, 6 May 2025 22:41:27 +0100 Subject: [PATCH 083/172] Update volatility3/framework/plugins/windows/thrdscan.py --- volatility3/framework/plugins/windows/thrdscan.py | 14 ++------------ 1 file changed, 2 insertions(+), 12 deletions(-) diff --git a/volatility3/framework/plugins/windows/thrdscan.py b/volatility3/framework/plugins/windows/thrdscan.py index 81ed9976f..d5a1a0b07 100644 --- a/volatility3/framework/plugins/windows/thrdscan.py +++ b/volatility3/framework/plugins/windows/thrdscan.py @@ -143,18 +143,8 @@ class ThrdScan(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface) else None ) else: - vads = None - - start_path = ( - pe_symbols.PESymbols.filepath_for_address(vads, thread_start_addr) - if vads - else None - ) - win32start_path = ( - pe_symbols.PESymbols.filepath_for_address(vads, thread_win32start_addr) - if vads - else None - ) + start_path = None + win32start_path = None return cls.ThreadInfo( thread_offset, From 606fecf5f81b5adcc5fee1ea82b870ceafb826d1 Mon Sep 17 00:00:00 2001 From: ikelos Date: Tue, 6 May 2025 22:41:33 +0100 Subject: [PATCH 084/172] Update volatility3/framework/plugins/windows/vadyarascan.py --- volatility3/framework/plugins/windows/vadyarascan.py | 1 - 1 file changed, 1 deletion(-) diff --git a/volatility3/framework/plugins/windows/vadyarascan.py b/volatility3/framework/plugins/windows/vadyarascan.py index 62d3f8862..04b9aadd9 100644 --- a/volatility3/framework/plugins/windows/vadyarascan.py +++ b/volatility3/framework/plugins/windows/vadyarascan.py @@ -99,7 +99,6 @@ class VadYaraScan(interfaces.plugins.PluginInterface): layer_name=layer.name, length=len(value), ) - yield 0, ( format_hints.Hex(offset), task.UniqueProcessId, From 94a029232848f34e0c8366ef145509a436d41c27 Mon Sep 17 00:00:00 2001 From: SolitudePy <47316655+SolitudePy@users.noreply.github.com> Date: Wed, 7 May 2025 19:05:05 +0300 Subject: [PATCH 085/172] add functions to etwpatch --- volatility3/framework/plugins/windows/etwpatch.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/volatility3/framework/plugins/windows/etwpatch.py b/volatility3/framework/plugins/windows/etwpatch.py index 14fbacc28..2388bdcc7 100644 --- a/volatility3/framework/plugins/windows/etwpatch.py +++ b/volatility3/framework/plugins/windows/etwpatch.py @@ -30,10 +30,17 @@ class EtwPatch(interfaces.plugins.PluginInterface): "EtwEventWrite", "EtwEventWriteFull", "NtTraceEvent", + "NtTraceEvent", + "ZwTraceEvent", + "NtTraceControl", + "ZwTraceControl", + "EtwpEventWriteFull" ], }, "advapi32.dll": { - pe_symbols.wanted_names_identifier: ["EventWrite"], + pe_symbols.wanted_names_identifier: [ + "EventWrite", + "TraceEvent"], }, } From fc7e13f6f881f59a11ce428e9880608f2517b677 Mon Sep 17 00:00:00 2001 From: SolitudePy <47316655+SolitudePy@users.noreply.github.com> Date: Wed, 7 May 2025 19:30:58 +0300 Subject: [PATCH 086/172] black --- volatility3/framework/plugins/windows/etwpatch.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/volatility3/framework/plugins/windows/etwpatch.py b/volatility3/framework/plugins/windows/etwpatch.py index 2388bdcc7..fb6375fd4 100644 --- a/volatility3/framework/plugins/windows/etwpatch.py +++ b/volatility3/framework/plugins/windows/etwpatch.py @@ -34,13 +34,11 @@ class EtwPatch(interfaces.plugins.PluginInterface): "ZwTraceEvent", "NtTraceControl", "ZwTraceControl", - "EtwpEventWriteFull" + "EtwpEventWriteFull", ], }, "advapi32.dll": { - pe_symbols.wanted_names_identifier: [ - "EventWrite", - "TraceEvent"], + pe_symbols.wanted_names_identifier: ["EventWrite", "TraceEvent"], }, } From 9ddf2d9abb5bb4bf820633e5345a033d6e70560f Mon Sep 17 00:00:00 2001 From: SolitudePy <47316655+SolitudePy@users.noreply.github.com> Date: Wed, 7 May 2025 19:55:30 +0300 Subject: [PATCH 087/172] refs --- volatility3/framework/plugins/windows/etwpatch.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/volatility3/framework/plugins/windows/etwpatch.py b/volatility3/framework/plugins/windows/etwpatch.py index fb6375fd4..b392f07ca 100644 --- a/volatility3/framework/plugins/windows/etwpatch.py +++ b/volatility3/framework/plugins/windows/etwpatch.py @@ -12,6 +12,8 @@ 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. @@ -80,7 +82,6 @@ class EtwPatch(interfaces.plugins.PluginInterface): kernel_module_name=self.config["kernel"], filter_func=filter_func, ): - try: proc_id = proc.UniqueProcessId proc_name = utility.array_to_string(proc.ImageFileName) From f719efb9ec88317ae5af87586d1351673bb858a4 Mon Sep 17 00:00:00 2001 From: SolitudePy <47316655+SolitudePy@users.noreply.github.com> Date: Wed, 7 May 2025 20:39:46 +0300 Subject: [PATCH 088/172] dupe --- volatility3/framework/plugins/windows/etwpatch.py | 1 - 1 file changed, 1 deletion(-) diff --git a/volatility3/framework/plugins/windows/etwpatch.py b/volatility3/framework/plugins/windows/etwpatch.py index b392f07ca..476d0eea7 100644 --- a/volatility3/framework/plugins/windows/etwpatch.py +++ b/volatility3/framework/plugins/windows/etwpatch.py @@ -32,7 +32,6 @@ class EtwPatch(interfaces.plugins.PluginInterface): "EtwEventWrite", "EtwEventWriteFull", "NtTraceEvent", - "NtTraceEvent", "ZwTraceEvent", "NtTraceControl", "ZwTraceControl", From 92eae9c50f56a486f0d62d57a68135d2b7375001 Mon Sep 17 00:00:00 2001 From: eve Date: Fri, 9 May 2025 09:06:43 +0100 Subject: [PATCH 089/172] Linux: Add comment for explaining the behaviour with smear --- volatility3/framework/symbols/linux/__init__.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/volatility3/framework/symbols/linux/__init__.py b/volatility3/framework/symbols/linux/__init__.py index 65f20d88d..327c6dd37 100644 --- a/volatility3/framework/symbols/linux/__init__.py +++ b/volatility3/framework/symbols/linux/__init__.py @@ -99,7 +99,7 @@ class LinuxKernelIntermedSymbols(intermed.IntermediateSymbolTable): class LinuxUtilities(interfaces.configuration.VersionableInterface): """Class with multiple useful linux functions.""" - _version = (2, 3, 0) + _version = (2, 3, 1) _required_framework_version = (2, 0, 0) framework.require_interface_version(*_required_framework_version) @@ -200,6 +200,9 @@ class LinuxUtilities(interfaces.configuration.VersionableInterface): path = "/" + "/".join(reversed(path_reversed)) if smeared: + # if there is smear the missing dname will be empty. e.g. if the normal + # path would be /foo/bar/baz, but bar is missing due to smear the results + # returned here will show /foo//baz. Note the // for the missing dname. return f" {path}" return path From e0abdd92f2889e3841197573393d87c8c31d2a68 Mon Sep 17 00:00:00 2001 From: David McDonald Date: Tue, 13 May 2025 17:52:12 -0500 Subject: [PATCH 090/172] Windows Thrdscan: Fix broken tuple unpacking Timeliner fails due to an incorrect unpacking of this tuple, which needs 3 additional dictionary items for start path, win32 start path, and win32 start address. --- volatility3/framework/plugins/windows/thrdscan.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/volatility3/framework/plugins/windows/thrdscan.py b/volatility3/framework/plugins/windows/thrdscan.py index d5a1a0b07..082b82284 100644 --- a/volatility3/framework/plugins/windows/thrdscan.py +++ b/volatility3/framework/plugins/windows/thrdscan.py @@ -190,6 +190,9 @@ class ThrdScan(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface) row_dict["PID"], row_dict["TID"], row_dict["StartAddress"], + row_dict["StartPath"], + row_dict["Win32StartAddress"], + row_dict["Win32StartPath"], row_dict["CreateTime"], row_dict["ExitTime"], ) = row_data From 90a3829ee766f3ef5530ef061389e7f343ba96b8 Mon Sep 17 00:00:00 2001 From: David McDonald Date: Thu, 15 May 2025 17:28:26 -0500 Subject: [PATCH 091/172] Windows Timeliner: Add basic test This is enough to ensure that the return code is nonzero and there was some valid output. --- test/plugins/windows/windows.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/test/plugins/windows/windows.py b/test/plugins/windows/windows.py index 4272b64d2..0f44ba533 100644 --- a/test/plugins/windows/windows.py +++ b/test/plugins/windows/windows.py @@ -58,6 +58,14 @@ 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): From f79d0cb5ef02c6e88174bdf341f38e6d3e9e8286 Mon Sep 17 00:00:00 2001 From: j-t-1 <120829237+j-t-1@users.noreply.github.com> Date: Fri, 16 May 2025 20:10:50 +0100 Subject: [PATCH 092/172] Align SeCreateTokenPrivilege with the other privileges --- .../framework/plugins/windows/sids_and_privileges.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/volatility3/framework/plugins/windows/sids_and_privileges.json b/volatility3/framework/plugins/windows/sids_and_privileges.json index 0378699ac..4333617d0 100644 --- a/volatility3/framework/plugins/windows/sids_and_privileges.json +++ b/volatility3/framework/plugins/windows/sids_and_privileges.json @@ -573,7 +573,7 @@ ["S-1-5-21-[0-9-]+-553$", "Remote Access Services (RAS)"] ], "privileges":{ - "2": ["SeCreateTokenPrivilege", "Create a token object"], + "2": ["SeCreateTokenPrivilege", "Create a token object"], "3": ["SeAssignPrimaryTokenPrivilege", "Replace a process-level token"], "4": ["SeLockMemoryPrivilege", "Lock pages in memory"], "5": ["SeIncreaseQuotaPrivilege", "Increase quotas"], @@ -609,4 +609,4 @@ "35": ["SeCreateSymbolicLinkPrivilege", "Required to create a symbolic link"], "36": ["SeDelegateSessionUserImpersonatePrivilege", "Obtain an impersonation token for another user in the same session."] } -} \ No newline at end of file +} From c1d1c66daf333f465fc780a3018f71acbedffa7c Mon Sep 17 00:00:00 2001 From: j-t-1 <120829237+j-t-1@users.noreply.github.com> Date: Sat, 17 May 2025 09:28:49 +0100 Subject: [PATCH 093/172] Remove extra word in comment --- volatility3/framework/plugins/mac/pslist.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/volatility3/framework/plugins/mac/pslist.py b/volatility3/framework/plugins/mac/pslist.py index 8c5e5c1a5..19f4516ac 100644 --- a/volatility3/framework/plugins/mac/pslist.py +++ b/volatility3/framework/plugins/mac/pslist.py @@ -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: From 4eef4c00843a02b1cd64ad71a92d082ad58be5d2 Mon Sep 17 00:00:00 2001 From: j-t-1 <120829237+j-t-1@users.noreply.github.com> Date: Sat, 17 May 2025 09:30:15 +0100 Subject: [PATCH 094/172] Remove extra word in comment --- doc/source/getting-started-windows-tutorial.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/source/getting-started-windows-tutorial.rst b/doc/source/getting-started-windows-tutorial.rst index 979cf1d96..3896000a2 100644 --- a/doc/source/getting-started-windows-tutorial.rst +++ b/doc/source/getting-started-windows-tutorial.rst @@ -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 ~~~~~~~~~~~~~~~~ From c93d2248d10db7c2b5af603b1050ab9685c31a14 Mon Sep 17 00:00:00 2001 From: j-t-1 <120829237+j-t-1@users.noreply.github.com> Date: Sat, 17 May 2025 09:30:52 +0100 Subject: [PATCH 095/172] Remove extra word in comment --- doc/source/getting-started-mac-tutorial.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/source/getting-started-mac-tutorial.rst b/doc/source/getting-started-mac-tutorial.rst index 61af7089b..f4889d689 100644 --- a/doc/source/getting-started-mac-tutorial.rst +++ b/doc/source/getting-started-mac-tutorial.rst @@ -37,7 +37,7 @@ For plugin requests, please create an issue with a description of the requested mac.check_sysctl.Check_sysctl mac.check_trap_table.Check_trap_table -.. note:: Here the the command is piped to grep and head 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 From 8bc2271e6f3c892b65f012717cadd22ef3b2a7fd Mon Sep 17 00:00:00 2001 From: j-t-1 <120829237+j-t-1@users.noreply.github.com> Date: Sat, 17 May 2025 09:31:19 +0100 Subject: [PATCH 096/172] Remove extra word in comment --- doc/source/getting-started-linux-tutorial.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/source/getting-started-linux-tutorial.rst b/doc/source/getting-started-linux-tutorial.rst index a1aad235d..031b49636 100644 --- a/doc/source/getting-started-linux-tutorial.rst +++ b/doc/source/getting-started-linux-tutorial.rst @@ -40,7 +40,7 @@ For plugin requests, please create an issue with a description of the requested linux.check_creds.Check_creds linux.check_idt.Check_idt -.. note:: Here the the command is piped to grep and head to provide the start of the list of linux plugins. +.. note:: Here the command is piped to grep and head to provide the start of the list of linux plugins. Using plugins From 6269091be761e3906ab184665d33cd3a85cdf1f5 Mon Sep 17 00:00:00 2001 From: Mike Auty Date: Sun, 18 May 2025 20:08:25 +0100 Subject: [PATCH 097/172] CLI: Support listing/clearing breakpoints in volshell --- volatility3/cli/volshell/generic.py | 44 +++++++++++++++++++++++++++-- 1 file changed, 42 insertions(+), 2 deletions(-) diff --git a/volatility3/cli/volshell/generic.py b/volatility3/cli/volshell/generic.py index 4c90c39c9..e68b0334e 100644 --- a/volatility3/cli/volshell/generic.py +++ b/volatility3/cli/volshell/generic.py @@ -187,6 +187,8 @@ 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), @@ -827,10 +829,13 @@ class Volshell(interfaces.plugins.PluginInterface): 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() - print("Hit breakpoint") + _ = "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()) @@ -838,11 +843,46 @@ class Volshell(interfaces.plugins.PluginInterface): setattr(layer, "read", wrapped_read) # Add the new breakpoint - print(f"Setting breakpoint {address:x} on {layer.name}") + 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""" From cb3e18ec9e354843cd4cff2f0fe6ba9befda6efd Mon Sep 17 00:00:00 2001 From: Elad Levi <99.elad.levi@gmail.com> Date: Mon, 19 May 2025 13:24:58 +0300 Subject: [PATCH 098/172] Update hashdump.py calling `.Name` returns array, changed to `.get_name()` which returns a string (the if statement in the code didn't work before this change) --- volatility3/framework/plugins/windows/registry/hashdump.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/volatility3/framework/plugins/windows/registry/hashdump.py b/volatility3/framework/plugins/windows/registry/hashdump.py index 1883d4530..256f15d61 100644 --- a/volatility3/framework/plugins/windows/registry/hashdump.py +++ b/volatility3/framework/plugins/windows/registry/hashdump.py @@ -350,7 +350,7 @@ 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]: From 2180c07c03cd69788f4d5d760d65ee47ad13c6f8 Mon Sep 17 00:00:00 2001 From: geekscrapy <11225502+geekscrapy@users.noreply.github.com> Date: Mon, 19 May 2025 14:47:29 +0400 Subject: [PATCH 099/172] Make nocase accessible --- volatility3/framework/plugins/yarascan.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/volatility3/framework/plugins/yarascan.py b/volatility3/framework/plugins/yarascan.py index 49db2af02..040a50c1a 100644 --- a/volatility3/framework/plugins/yarascan.py +++ b/volatility3/framework/plugins/yarascan.py @@ -182,7 +182,7 @@ class YaraScan(plugins.PluginInterface): rule = config["yara_string"] if rule[0] not in ["{", "/"]: rule = f'"{rule}"' - if config.get("case", False): + if config.get("insensitive", False): rule += " nocase" if config.get("wide", False): rule += " wide ascii" From 4f11586ef19bff42f28298c98ad943f769200751 Mon Sep 17 00:00:00 2001 From: Jost Alemann Date: Thu, 22 May 2025 23:25:15 +0200 Subject: [PATCH 100/172] fix: typos --- development/compare-vol.py | 2 +- volatility3/cli/text_renderer.py | 4 ++-- volatility3/cli/volshell/generic.py | 4 ++-- volatility3/framework/__init__.py | 2 +- volatility3/framework/automagic/symbol_cache.py | 2 +- volatility3/framework/contexts/__init__.py | 2 +- volatility3/framework/interfaces/context.py | 2 +- volatility3/framework/layers/intel.py | 2 +- volatility3/framework/objects/utility.py | 2 +- volatility3/framework/plugins/layerwriter.py | 2 +- volatility3/framework/plugins/linux/capabilities.py | 2 +- volatility3/framework/plugins/linux/netfilter.py | 4 ++-- .../framework/plugins/linux/tracing/ftrace.py | 2 +- .../framework/plugins/linux/tracing/tracepoints.py | 2 +- volatility3/framework/plugins/linux/vmaregexscan.py | 2 +- volatility3/framework/plugins/mac/pslist.py | 2 +- volatility3/framework/plugins/regexscan.py | 2 +- volatility3/framework/plugins/vmscan.py | 2 +- .../framework/plugins/windows/direct_system_calls.py | 4 ++-- .../plugins/windows/orphan_kernel_threads.py | 2 +- volatility3/framework/plugins/windows/pe_symbols.py | 8 ++++---- .../framework/plugins/windows/processghosting.py | 2 +- .../framework/plugins/windows/registry/hashdump.py | 2 +- .../plugins/windows/registry/scheduled_tasks.py | 4 ++-- .../framework/plugins/windows/shimcachemem.py | 2 +- .../framework/plugins/windows/suspicious_threads.py | 2 +- .../framework/plugins/windows/vadregexscan.py | 2 +- volatility3/framework/renderers/__init__.py | 2 +- volatility3/framework/symbols/linux/__init__.py | 2 +- .../framework/symbols/linux/extensions/__init__.py | 12 ++++++------ .../framework/symbols/linux/extensions/network.py | 6 +++--- volatility3/framework/symbols/linux/kallsyms.py | 2 +- .../symbols/linux/utilities/module_extract.py | 8 ++++---- .../framework/symbols/linux/utilities/modules.py | 6 +++--- .../framework/symbols/windows/extensions/consoles.py | 2 +- .../framework/symbols/windows/extensions/gui.py | 2 +- 36 files changed, 56 insertions(+), 56 deletions(-) diff --git a/development/compare-vol.py b/development/compare-vol.py index a01d8e93c..717d81d3e 100644 --- a/development/compare-vol.py +++ b/development/compare-vol.py @@ -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", diff --git a/volatility3/cli/text_renderer.py b/volatility3/cli/text_renderer.py index 044f33ed1..1e437a0be 100644 --- a/volatility3/cli/text_renderer.py +++ b/volatility3/cli/text_renderer.py @@ -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 diff --git a/volatility3/cli/volshell/generic.py b/volatility3/cli/volshell/generic.py index e68b0334e..ace4b2119 100644 --- a/volatility3/cli/volshell/generic.py +++ b/volatility3/cli/volshell/generic.py @@ -485,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 = ":" @@ -558,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)) diff --git a/volatility3/framework/__init__.py b/volatility3/framework/__init__.py index 0bbdefa43..c384542a8 100644 --- a/volatility3/framework/__init__.py +++ b/volatility3/framework/__init__.py @@ -218,4 +218,4 @@ def clear_cache(complete=True): os.unlink(cache_filename) os.unlink(os.path.join(constants.CACHE_PATH, constants.IDENTIFIERS_FILENAME)) except FileNotFoundError: - vollog.log(constants.LOGLEVEL_VVVV, "Attempting to clear a non-existant cache") + vollog.log(constants.LOGLEVEL_VVVV, "Attempting to clear a non-existent cache") diff --git a/volatility3/framework/automagic/symbol_cache.py b/volatility3/framework/automagic/symbol_cache.py index cd1a348a4..327575e96 100644 --- a/volatility3/framework/automagic/symbol_cache.py +++ b/volatility3/framework/automagic/symbol_cache.py @@ -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"]: diff --git a/volatility3/framework/contexts/__init__.py b/volatility3/framework/contexts/__init__.py index a000fce90..32d33f657 100644 --- a/volatility3/framework/contexts/__init__.py +++ b/volatility3/framework/contexts/__init__.py @@ -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 diff --git a/volatility3/framework/interfaces/context.py b/volatility3/framework/interfaces/context.py index 723f2fd46..4f863898e 100644 --- a/volatility3/framework/interfaces/context.py +++ b/volatility3/framework/interfaces/context.py @@ -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 diff --git a/volatility3/framework/layers/intel.py b/volatility3/framework/layers/intel.py index 1069b7f6d..696c33353 100644 --- a/volatility3/framework/layers/intel.py +++ b/volatility3/framework/layers/intel.py @@ -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): diff --git a/volatility3/framework/objects/utility.py b/volatility3/framework/objects/utility.py index ef702060c..59ac0ee55 100644 --- a/volatility3/framework/objects/utility.py +++ b/volatility3/framework/objects/utility.py @@ -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 diff --git a/volatility3/framework/plugins/layerwriter.py b/volatility3/framework/plugins/layerwriter.py index 10e7a7a72..bcc999aba 100644 --- a/volatility3/framework/plugins/layerwriter.py +++ b/volatility3/framework/plugins/layerwriter.py @@ -62,7 +62,7 @@ class LayerWriter(plugins.PluginInterface): Args: context: the context from which to read the memory layer layer_name: the name of the layer to write out - preferred_name: a string with the preferred filename for hte file + preferred_name: a string with the preferred filename for the file chunk_size: an optional size for the chunks that should be written (defaults to 0x500000) open_method: class for creating FileHandler context managers progress_callback: an optional function that takes a percentage and a string that displays output diff --git a/volatility3/framework/plugins/linux/capabilities.py b/volatility3/framework/plugins/linux/capabilities.py index dae6aac6a..364047893 100644 --- a/volatility3/framework/plugins/linux/capabilities.py +++ b/volatility3/framework/plugins/linux/capabilities.py @@ -29,7 +29,7 @@ class TaskData: @dataclass class CapabilitiesData: - """Stores each set of capabilties for a task""" + """Stores each set of capabilities for a task""" cap_inheritable: interfaces.objects.ObjectInterface cap_permitted: interfaces.objects.ObjectInterface diff --git a/volatility3/framework/plugins/linux/netfilter.py b/volatility3/framework/plugins/linux/netfilter.py index 9079adef9..d724d4296 100644 --- a/volatility3/framework/plugins/linux/netfilter.py +++ b/volatility3/framework/plugins/linux/netfilter.py @@ -417,7 +417,7 @@ class NetfilterImp_to_4_3(AbstractNetfilter): class NetfilterImp_4_3_to_4_9(AbstractNetfilter): - """Netfilter hooks were added to network namepaces in 4.3. + """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. @@ -611,7 +611,7 @@ class NetfilterImp_4_16_to_latest(NetfilterImp_4_14_to_4_16): 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. + implementations. Netfilter NetDev hooks are set per network device which belongs to a network namespace. diff --git a/volatility3/framework/plugins/linux/tracing/ftrace.py b/volatility3/framework/plugins/linux/tracing/ftrace.py index afcc71784..4b7c2ebb3 100644 --- a/volatility3/framework/plugins/linux/tracing/ftrace.py +++ b/volatility3/framework/plugins/linux/tracing/ftrace.py @@ -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 diff --git a/volatility3/framework/plugins/linux/tracing/tracepoints.py b/volatility3/framework/plugins/linux/tracing/tracepoints.py index 25c87b664..8666ffd6a 100644 --- a/volatility3/framework/plugins/linux/tracing/tracepoints.py +++ b/volatility3/framework/plugins/linux/tracing/tracepoints.py @@ -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 diff --git a/volatility3/framework/plugins/linux/vmaregexscan.py b/volatility3/framework/plugins/linux/vmaregexscan.py index 37a1a5940..fb757beb5 100644 --- a/volatility3/framework/plugins/linux/vmaregexscan.py +++ b/volatility3/framework/plugins/linux/vmaregexscan.py @@ -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: diff --git a/volatility3/framework/plugins/mac/pslist.py b/volatility3/framework/plugins/mac/pslist.py index 19f4516ac..904e4e201 100644 --- a/volatility3/framework/plugins/mac/pslist.py +++ b/volatility3/framework/plugins/mac/pslist.py @@ -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 diff --git a/volatility3/framework/plugins/regexscan.py b/volatility3/framework/plugins/regexscan.py index 343753e92..7df89f841 100644 --- a/volatility3/framework/plugins/regexscan.py +++ b/volatility3/framework/plugins/regexscan.py @@ -56,7 +56,7 @@ class RegExScan(plugins.PluginInterface): ): result_data = 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: diff --git a/volatility3/framework/plugins/vmscan.py b/volatility3/framework/plugins/vmscan.py index 5322456b5..19d997605 100644 --- a/volatility3/framework/plugins/vmscan.py +++ b/volatility3/framework/plugins/vmscan.py @@ -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) diff --git a/volatility3/framework/plugins/windows/direct_system_calls.py b/volatility3/framework/plugins/windows/direct_system_calls.py index 60dbf728c..dce09605b 100644 --- a/volatility3/framework/plugins/windows/direct_system_calls.py +++ b/volatility3/framework/plugins/windows/direct_system_calls.py @@ -42,7 +42,7 @@ 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 +`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 @@ -116,7 +116,7 @@ class DirectSystemCalls(interfaces.plugins.PluginInterface): address: int, ) -> Optional[Tuple[str, "capstone._cs_insn"]]: """ - Determines if the bytes starting at `data` represent a valid syscall instrunction invocation block + 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: diff --git a/volatility3/framework/plugins/windows/orphan_kernel_threads.py b/volatility3/framework/plugins/windows/orphan_kernel_threads.py index b4dec0fc5..16a25503e 100644 --- a/volatility3/framework/plugins/windows/orphan_kernel_threads.py +++ b/volatility3/framework/plugins/windows/orphan_kernel_threads.py @@ -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 diff --git a/volatility3/framework/plugins/windows/pe_symbols.py b/volatility3/framework/plugins/windows/pe_symbols.py index e3af0c28a..00c6fd868 100644 --- a/volatility3/framework/plugins/windows/pe_symbols.py +++ b/volatility3/framework/plugins/windows/pe_symbols.py @@ -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: diff --git a/volatility3/framework/plugins/windows/processghosting.py b/volatility3/framework/plugins/windows/processghosting.py index 7e7f6d3cc..f234bc2e7 100644 --- a/volatility3/framework/plugins/windows/processghosting.py +++ b/volatility3/framework/plugins/windows/processghosting.py @@ -118,7 +118,7 @@ class ProcessGhosting(interfaces.plugins.PluginInterface): Args: proc: - mapped_files: A dictionary mapping vad base addreses to the path and vad instance for the process + 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 diff --git a/volatility3/framework/plugins/windows/registry/hashdump.py b/volatility3/framework/plugins/windows/registry/hashdump.py index 256f15d61..19bd60e81 100644 --- a/volatility3/framework/plugins/windows/registry/hashdump.py +++ b/volatility3/framework/plugins/windows/registry/hashdump.py @@ -355,7 +355,7 @@ class Hashdump(interfaces.plugins.PluginInterface): @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" diff --git a/volatility3/framework/plugins/windows/registry/scheduled_tasks.py b/volatility3/framework/plugins/windows/registry/scheduled_tasks.py index a2789e5df..2c660229b 100644 --- a/volatility3/framework/plugins/windows/registry/scheduled_tasks.py +++ b/volatility3/framework/plugins/windows/registry/scheduled_tasks.py @@ -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 diff --git a/volatility3/framework/plugins/windows/shimcachemem.py b/volatility3/framework/plugins/windows/shimcachemem.py index 5c0af7766..7a03ebbb6 100644 --- a/volatility3/framework/plugins/windows/shimcachemem.py +++ b/volatility3/framework/plugins/windows/shimcachemem.py @@ -236,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 diff --git a/volatility3/framework/plugins/windows/suspicious_threads.py b/volatility3/framework/plugins/windows/suspicious_threads.py index eabc637c8..3da8cb21a 100644 --- a/volatility3/framework/plugins/windows/suspicious_threads.py +++ b/volatility3/framework/plugins/windows/suspicious_threads.py @@ -129,7 +129,7 @@ class SuspiciousThreads(interfaces.plugins.PluginInterface): ): yield ( vad_path, - "VAD at base address ({vad_base:#x}) hosting this thread maps an application executable that is not the process exectuable", + "VAD at base address ({vad_base:#x}) hosting this thread maps an application executable that is not the process executable", ) def _enumerate_processes( diff --git a/volatility3/framework/plugins/windows/vadregexscan.py b/volatility3/framework/plugins/windows/vadregexscan.py index 5ead4e453..068838e35 100644 --- a/volatility3/framework/plugins/windows/vadregexscan.py +++ b/volatility3/framework/plugins/windows/vadregexscan.py @@ -86,7 +86,7 @@ class VadRegExScan(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: diff --git a/volatility3/framework/renderers/__init__.py b/volatility3/framework/renderers/__init__.py index 4f1de586a..8732e6e88 100644 --- a/volatility3/framework/renderers/__init__.py +++ b/volatility3/framework/renderers/__init__.py @@ -93,7 +93,7 @@ class Disassembly(interfaces.renderers.BasicType): class LayerData(interfaces.renderers.BasicType): """Layer data - This requires the contex to be passed in, in case plugins want to use multiple contexts + This requires the context to be passed in, in case plugins want to use multiple contexts and to ensure the TreeGrid interface doesn't change, since this would break all existing plugins """ diff --git a/volatility3/framework/symbols/linux/__init__.py b/volatility3/framework/symbols/linux/__init__.py index 327c6dd37..ac27b7e42 100644 --- a/volatility3/framework/symbols/linux/__init__.py +++ b/volatility3/framework/symbols/linux/__init__.py @@ -515,7 +515,7 @@ class LinuxUtilities(interfaces.configuration.VersionableInterface): vmlinux: interfaces.context.ModuleInterface, ) -> Optional[interfaces.objects.ObjectInterface]: """Cast a member of a structure out to the containing structure. - It mimicks the Linux kernel macro container_of() see include/linux.kernel.h + It mimics the Linux kernel macro container_of() see include/linux.kernel.h Args: addr: The pointer to the member. diff --git a/volatility3/framework/symbols/linux/extensions/__init__.py b/volatility3/framework/symbols/linux/extensions/__init__.py index c980ee6b1..3b9a73e7c 100644 --- a/volatility3/framework/symbols/linux/extensions/__init__.py +++ b/volatility3/framework/symbols/linux/extensions/__init__.py @@ -984,9 +984,9 @@ class maple_tree(objects.StructType): current_depth + 1, ) else: - # unkown maple node type + # unknown maple node type raise AttributeError( - f"Unkown Maple Tree node type {node_type} at offset {hex(pointer)}." + f"Unknown Maple Tree node type {node_type} at offset {hex(pointer)}." ) @@ -2295,7 +2295,7 @@ class kernel_cap_t(kernel_cap_struct): class Timespec64Abstract(abc.ABC): - """Abstract class to handle all required timespec64 operations, convertions and + """Abstract class to handle all required timespec64 operations, conversions and adjustments.""" @classmethod @@ -2391,7 +2391,7 @@ class Timespec64Abstract(abc.ABC): class Timespec64Concrete(Timespec64Abstract): - """Handle all required timespec64 operations, convertions and adjustments. + """Handle all required timespec64 operations, conversions and adjustments. This is used to dynamically create timespec64-like objects, each with its own variables and the same methods as a timespec64 object extension. """ @@ -2402,7 +2402,7 @@ class Timespec64Concrete(Timespec64Abstract): class timespec64(Timespec64Abstract, objects.StructType): - """Handle all required timespec64 operations, convertions and adjustments. + """Handle all required timespec64 operations, conversions and adjustments. This works as an extension of the timespec64 object while maintaining the same methods as a Timespec64Concrete object. """ @@ -2770,7 +2770,7 @@ class IDR(objects.StructType): vmlinux = linux.LinuxUtilities.get_module_from_volobj_type(self._context, self) if not vmlinux.get_type("idr_layer").has_member("layer"): vollog.info( - "Unsupported IDR implementation, it should be a very very old kernel, probabably < 2.6" + "Unsupported IDR implementation, it should be a very very old kernel, probably < 2.6" ) return None diff --git a/volatility3/framework/symbols/linux/extensions/network.py b/volatility3/framework/symbols/linux/extensions/network.py index 30094d1b1..37fa5a41f 100644 --- a/volatility3/framework/symbols/linux/extensions/network.py +++ b/volatility3/framework/symbols/linux/extensions/network.py @@ -65,7 +65,7 @@ class net_device(objects.StructType): hwaddr = parent_layer.read(self.dev_addr, self.addr_len, pad=True) except exceptions.InvalidAddressException: vollog.debug( - f"Unable to read network inteface mac address from {self.dev_addr:#x}" + f"Unable to read network interface mac address from {self.dev_addr:#x}" ) return None @@ -255,10 +255,10 @@ class net_device(objects.StructType): return None def get_queue_length(self) -> int: - """Return the netwrok device transmision qeueue length (qlen) + """Return the network device transmission queue length (qlen) Returns: - int: the netwrok device transmision qeueue length (qlen) + int: the network device transmission queue length (qlen) """ return self.tx_queue_len diff --git a/volatility3/framework/symbols/linux/kallsyms.py b/volatility3/framework/symbols/linux/kallsyms.py index 35aba3ca5..be026fc4e 100644 --- a/volatility3/framework/symbols/linux/kallsyms.py +++ b/volatility3/framework/symbols/linux/kallsyms.py @@ -121,7 +121,7 @@ class _KallsymsIO: self._endian = endian def read(self, size: int) -> bytes: - """Return 'size' bytes from the current postion""" + """Return 'size' bytes from the current position""" layer = self._context.layers[self._layer_name] buf = layer.read(offset=self._position, length=size) self._position += size diff --git a/volatility3/framework/symbols/linux/utilities/module_extract.py b/volatility3/framework/symbols/linux/utilities/module_extract.py index 49ad4d9c0..e55f77668 100644 --- a/volatility3/framework/symbols/linux/utilities/module_extract.py +++ b/volatility3/framework/symbols/linux/utilities/module_extract.py @@ -21,7 +21,7 @@ from volatility3.framework.symbols.linux import extensions vollog = logging.getLogger(__name__) -# This module is responsbile for producing an ELF file of a kernel module (LKM) loaded in memory +# This module is responsible for producing an ELF file of a kernel module (LKM) loaded in memory # This extraction task is quite complicated as the Linux kernel discards the ELF header at load time # Due to this, to support static analysis, we must create an ELF header and proper file based on the sections # There are also several other significant complications that we must deal with when trying to extract an LKM @@ -423,7 +423,7 @@ class ModuleExtract(interfaces.configuration.VersionableInterface): ) if not data: vollog.debug( - f"Coult not construct a symbol table for module at {module.vol.offset}. Cannot recover." + f"Could not construct a symbol table for module at {module.vol.offset}. Cannot recover." ) return None, None, None @@ -469,7 +469,7 @@ class ModuleExtract(interfaces.configuration.VersionableInterface): e_shentsize_int = 64 header_size = 64 - e_type = struct.pack(" ModuleGathererInterface.gatherer_return_type: """ Returns a ModuleInfo instance that encodes the kernel - This is required to map function pointers to the kerenl executable + This is required to map function pointers to the kernel executable """ kernel = context.modules[kernel_module_name] diff --git a/volatility3/framework/symbols/windows/extensions/consoles.py b/volatility3/framework/symbols/windows/extensions/consoles.py index 9666fd79c..b16855b9c 100644 --- a/volatility3/framework/symbols/windows/extensions/consoles.py +++ b/volatility3/framework/symbols/windows/extensions/consoles.py @@ -168,7 +168,7 @@ class SCREEN_INFORMATION(objects.StructType): @param truncate: True if the empty rows at the end (i.e. bottom) of the screen buffer should be - supressed. + suppressed. """ rows = [] diff --git a/volatility3/framework/symbols/windows/extensions/gui.py b/volatility3/framework/symbols/windows/extensions/gui.py index d1835631f..0d39f8173 100644 --- a/volatility3/framework/symbols/windows/extensions/gui.py +++ b/volatility3/framework/symbols/windows/extensions/gui.py @@ -146,7 +146,7 @@ class GUIExtensions(interfaces.configuration.VersionableInterface): self, window, max_windows ) -> Generator[Tuple[interfaces.objects.ObjectInterface, str], None, None]: """ - Recusively walks and yields the adjacent and child windows + Recursively walks and yields the adjacent and child windows """ seen_windows = set() seen_children = set() From 40a3d23e2db14363efe219d7633e4f4de808503a Mon Sep 17 00:00:00 2001 From: Mike Auty Date: Mon, 26 May 2025 10:19:28 +0100 Subject: [PATCH 101/172] Infra: Update checkout actions --- .github/workflows/build-pyinstaller.yml | 2 +- .github/workflows/codeql.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build-pyinstaller.yml b/.github/workflows/build-pyinstaller.yml index b1d15eb10..7d9d86fb0 100644 --- a/.github/workflows/build-pyinstaller.yml +++ b/.github/workflows/build-pyinstaller.yml @@ -18,7 +18,7 @@ jobs: matrix: python-version: ["3.11"] steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v4 with: diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 324390e43..c79f1c086 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -38,7 +38,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL From 83cfc84f36fcb848ebe951404d4bfa7ebde510c5 Mon Sep 17 00:00:00 2001 From: eve Date: Wed, 28 May 2025 20:05:03 +0100 Subject: [PATCH 102/172] Add a deprecation warning for PluginRequirement --- volatility3/framework/configuration/requirements.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/volatility3/framework/configuration/requirements.py b/volatility3/framework/configuration/requirements.py index 3e3608000..166c02fe5 100644 --- a/volatility3/framework/configuration/requirements.py +++ b/volatility3/framework/configuration/requirements.py @@ -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 vollog = logging.getLogger(__name__) @@ -600,6 +600,11 @@ class VersionRequirement(interfaces.configuration.RequirementInterface): return True +@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, From 0be4d809c12fbe742b34c7f651182b6c535fc598 Mon Sep 17 00:00:00 2001 From: eve Date: Wed, 28 May 2025 21:16:04 +0100 Subject: [PATCH 103/172] Linux: update PerfEvents plugin to use VersionRequirement for pslist --- volatility3/framework/plugins/linux/tracing/perf_events.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/volatility3/framework/plugins/linux/tracing/perf_events.py b/volatility3/framework/plugins/linux/tracing/perf_events.py index 3e0a40579..23b2f2c72 100644 --- a/volatility3/framework/plugins/linux/tracing/perf_events.py +++ b/volatility3/framework/plugins/linux/tracing/perf_events.py @@ -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) ), ] From bb4cee7071fb071b8ee9a06ed1b2cd4d78af2b51 Mon Sep 17 00:00:00 2001 From: j-t-1 <120829237+j-t-1@users.noreply.github.com> Date: Thu, 29 May 2025 06:47:03 +0100 Subject: [PATCH 104/172] Tweak comments Also remove zeroes starting a slice. --- .../framework/plugins/windows/malfind.py | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/volatility3/framework/plugins/windows/malfind.py b/volatility3/framework/plugins/windows/malfind.py index d9861d9c8..4727d5ff0 100644 --- a/volatility3/framework/plugins/windows/malfind.py +++ b/volatility3/framework/plugins/windows/malfind.py @@ -61,7 +61,7 @@ class Malfind(interfaces.plugins.PluginInterface): vad: the MMVAD structure to test Returns: - A boolean indicating whether a vad is empty or not + A boolean indicating whether a VAD is empty or not """ CHUNK_SIZE = 0x1000 @@ -112,13 +112,13 @@ class Malfind(interfaces.plugins.PluginInterface): code. Args: - context: The context to retrieve required elements (layers, symbol tables) from + context: The context 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 containing in that region + An iterable of VAD instances and the first 64 bytes of data contained in that region """ proc_id = "Unknown" try: @@ -144,7 +144,7 @@ class Malfind(interfaces.plugins.PluginInterface): if not write_exec: """ # Inspect "PAGE_EXECUTE_READ" VAD pages to detect - # non writable memory regions having been injected + # non-writable memory regions having been injected # using elevated WriteProcessMemory(). """ if "EXECUTE" in protection_string: @@ -152,7 +152,7 @@ class Malfind(interfaces.plugins.PluginInterface): 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 we have a dirty page in a non-writable "EXECUTE" region, it is suspicious. if proc_layer.is_dirty(page): dirty_page = page break @@ -188,10 +188,10 @@ class Malfind(interfaces.plugins.PluginInterface): yield (vad, data) def _generator(self, procs): - # determine if we're on a 32 or 64 bit kernel + # 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 + # Set refined criteria to know when to add to "Notes" column refined_criteria = { b"MZ": "MZ header", b"\x55\x8b": "PE header", @@ -204,7 +204,7 @@ class Malfind(interfaces.plugins.PluginInterface): ) for proc in procs: - # by default, "Notes" column will be set to N/A + # 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( @@ -215,10 +215,10 @@ class Malfind(interfaces.plugins.PluginInterface): 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 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 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: From a4bc02e41f49de526a793d198d325ac5c0b944cf Mon Sep 17 00:00:00 2001 From: eve Date: Thu, 29 May 2025 07:26:53 +0100 Subject: [PATCH 105/172] Move version checking logic into versionutils module --- volatility3/framework/__init__.py | 20 ++++++---------- .../framework/configuration/requirements.py | 10 +++----- volatility3/framework/deprecation.py | 5 ++-- volatility3/framework/versionutils.py | 23 +++++++++++++++++++ 4 files changed, 35 insertions(+), 23 deletions(-) create mode 100644 volatility3/framework/versionutils.py diff --git a/volatility3/framework/__init__.py b/volatility3/framework/__init__.py index c384542a8..1c899f434 100644 --- a/volatility3/framework/__init__.py +++ b/volatility3/framework/__init__.py @@ -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: diff --git a/volatility3/framework/configuration/requirements.py b/volatility3/framework/configuration/requirements.py index 166c02fe5..b7971d727 100644 --- a/volatility3/framework/configuration/requirements.py +++ b/volatility3/framework/configuration/requirements.py @@ -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, deprecation +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,11 +593,7 @@ 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 + versionutils.matches_required(required, version) @deprecation.renamed_class( diff --git a/volatility3/framework/deprecation.py b/volatility3/framework/deprecation.py index 859a32bad..667ea72a5 100644 --- a/volatility3/framework/deprecation.py +++ b/volatility3/framework/deprecation.py @@ -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( diff --git a/volatility3/framework/versionutils.py b/volatility3/framework/versionutils.py new file mode 100644 index 000000000..334410ff8 --- /dev/null +++ b/volatility3/framework/versionutils.py @@ -0,0 +1,23 @@ +# This file is Copyright 2025 Volatility Foundation and licensed under the Volatility Software License 1.0 +# which is available at https://www.volatilityfoundation.org/license/vsl-v1.0 +# + +from typing import Tuple + + +def matches_required(required: Tuple[int, ...], version: Tuple[int, int, int]) -> bool: + """ + Checks if a version tuple satisfies the required version major and minor constraints. + + Parameters: + required (Tuple[int, ...]): A tuple containing required major and optionally minor version numbers. + version (Tuple[int, int, int]): A tuple containing the full version (major, minor, patch). + + Returns: + bool: True if the version matches the required constraints, False otherwise. + """ + if len(required) > 0 and version[0] != required[0]: + return False + if len(required) > 1 and version[1] < required[1]: + return False + return True From 1206b69abd362c750e4b7ab471950d5fe43726fe Mon Sep 17 00:00:00 2001 From: ikelos Date: Thu, 29 May 2025 09:45:26 +0100 Subject: [PATCH 106/172] Update volatility3/framework/plugins/windows/malfind.py --- volatility3/framework/plugins/windows/malfind.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/volatility3/framework/plugins/windows/malfind.py b/volatility3/framework/plugins/windows/malfind.py index 4727d5ff0..33eaf64ef 100644 --- a/volatility3/framework/plugins/windows/malfind.py +++ b/volatility3/framework/plugins/windows/malfind.py @@ -112,7 +112,7 @@ class Malfind(interfaces.plugins.PluginInterface): code. Args: - context: The context to retrieve required elements (layers, symbol tables) + 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 From 3ce515bc017b2bd19c1ac2dd7ab9f2d94d44c6e8 Mon Sep 17 00:00:00 2001 From: eve Date: Thu, 29 May 2025 16:53:35 +0100 Subject: [PATCH 107/172] Add return for matches_required in VersionRequirement --- volatility3/framework/configuration/requirements.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/volatility3/framework/configuration/requirements.py b/volatility3/framework/configuration/requirements.py index b7971d727..6d978e2a9 100644 --- a/volatility3/framework/configuration/requirements.py +++ b/volatility3/framework/configuration/requirements.py @@ -593,7 +593,7 @@ class VersionRequirement(interfaces.configuration.RequirementInterface): def matches_required( cls, required: Tuple[int, ...], version: Tuple[int, int, int] ) -> bool: - versionutils.matches_required(required, version) + return versionutils.matches_required(required, version) @deprecation.renamed_class( From 306b7ff42bde54830be3e1c34880d981b9c35743 Mon Sep 17 00:00:00 2001 From: SolitudePy <47316655+SolitudePy@users.noreply.github.com> Date: Fri, 6 Jun 2025 23:55:12 +0300 Subject: [PATCH 108/172] use maxsize argument --- volatility3/framework/plugins/regexscan.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/volatility3/framework/plugins/regexscan.py b/volatility3/framework/plugins/regexscan.py index 7df89f841..c5ec97fd6 100644 --- a/volatility3/framework/plugins/regexscan.py +++ b/volatility3/framework/plugins/regexscan.py @@ -49,24 +49,24 @@ class RegExScan(plugins.PluginInterface): def _generator(self, regex_pattern): regex_pattern = bytes(regex_pattern, "UTF-8") vollog.debug(f"RegEx Pattern: {regex_pattern}") - + maxsize = self.config.get("maxsize", self.MAXSIZE_DEFAULT) layer = self.context.layers[self.config["primary"]] for offset in layer.scan( context=self.context, scanner=scanners.RegExScanner(regex_pattern) ): - result_data = layer.read(offset, self.MAXSIZE_DEFAULT, pad=True) + result_data = layer.read(offset, maxsize, pad=True) # reapply the regex in order to extract just the match regex_result = re.match(regex_pattern, result_data) if regex_result: - # the match is within the results_data (e.g. it fits within MAXSIZE_DEFAULT) + # 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 From f01310c238d7b7d4f47814d4c482454f7b928dd0 Mon Sep 17 00:00:00 2001 From: SolitudePy <47316655+SolitudePy@users.noreply.github.com> Date: Sat, 7 Jun 2025 11:39:56 +0300 Subject: [PATCH 109/172] use search instead of match --- volatility3/framework/plugins/regexscan.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/volatility3/framework/plugins/regexscan.py b/volatility3/framework/plugins/regexscan.py index c5ec97fd6..4df00a583 100644 --- a/volatility3/framework/plugins/regexscan.py +++ b/volatility3/framework/plugins/regexscan.py @@ -48,6 +48,7 @@ class RegExScan(plugins.PluginInterface): def _generator(self, regex_pattern): regex_pattern = bytes(regex_pattern, "UTF-8") + compiled_pattern = re.compile(regex_pattern) vollog.debug(f"RegEx Pattern: {regex_pattern}") maxsize = self.config.get("maxsize", self.MAXSIZE_DEFAULT) layer = self.context.layers[self.config["primary"]] @@ -57,7 +58,7 @@ class RegExScan(plugins.PluginInterface): result_data = layer.read(offset, maxsize, pad=True) # reapply the regex in order to extract just the match - regex_result = re.match(regex_pattern, result_data) + regex_result = compiled_pattern.search(result_data) if regex_result: # the match is within the results_data (e.g. it fits within maxsize) From 89247766981b0d4afd40d886e248e9dbbab1c909 Mon Sep 17 00:00:00 2001 From: Mike Auty Date: Sat, 7 Jun 2025 09:41:19 +0100 Subject: [PATCH 110/172] Plugins: Remove deprecated PluginRequirement in favour of VersionRequirement --- volatility3/framework/plugins/mac/timers.py | 4 ++-- volatility3/framework/plugins/windows/timers.py | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/volatility3/framework/plugins/mac/timers.py b/volatility3/framework/plugins/mac/timers.py index 8a267bd55..aef8c5e1c 100644 --- a/volatility3/framework/plugins/mac/timers.py +++ b/volatility3/framework/plugins/mac/timers.py @@ -31,8 +31,8 @@ class Timers(plugins.PluginInterface): requirements.VersionRequirement( name="macutils", component=mac.MacUtilities, version=(1, 3, 0) ), - requirements.PluginRequirement( - name="lsmod", plugin=lsmod.Lsmod, version=(2, 0, 0) + requirements.VersionRequirement( + name="lsmod", component=lsmod.Lsmod, version=(2, 0, 0) ), ] diff --git a/volatility3/framework/plugins/windows/timers.py b/volatility3/framework/plugins/windows/timers.py index f530a4c7b..c7364d5a5 100644 --- a/volatility3/framework/plugins/windows/timers.py +++ b/volatility3/framework/plugins/windows/timers.py @@ -35,11 +35,11 @@ class Timers(interfaces.plugins.PluginInterface): description="Windows kernel", architectures=["Intel32", "Intel64"], ), - requirements.PluginRequirement( - name="ssdt", plugin=ssdt.SSDT, version=(2, 0, 0) + requirements.VersionRequirement( + name="ssdt", component=ssdt.SSDT, version=(2, 0, 0) ), - requirements.PluginRequirement( - name="kpcrs", plugin=kpcrs.KPCRs, version=(2, 0, 0) + requirements.VersionRequirement( + name="kpcrs", component=kpcrs.KPCRs, version=(2, 0, 0) ), ] From eba4e4b50c8842e286410bcd62a0930b0c9b9602 Mon Sep 17 00:00:00 2001 From: SolitudePy <47316655+SolitudePy@users.noreply.github.com> Date: Sat, 7 Jun 2025 11:50:07 +0300 Subject: [PATCH 111/172] generator overhead --- volatility3/framework/plugins/regexscan.py | 38 ++++++++++++++++++---- 1 file changed, 31 insertions(+), 7 deletions(-) diff --git a/volatility3/framework/plugins/regexscan.py b/volatility3/framework/plugins/regexscan.py index 4df00a583..e95986899 100644 --- a/volatility3/framework/plugins/regexscan.py +++ b/volatility3/framework/plugins/regexscan.py @@ -46,14 +46,12 @@ class RegExScan(plugins.PluginInterface): ), ] - def _generator(self, regex_pattern): - regex_pattern = bytes(regex_pattern, "UTF-8") - compiled_pattern = re.compile(regex_pattern) - vollog.debug(f"RegEx Pattern: {regex_pattern}") - maxsize = self.config.get("maxsize", self.MAXSIZE_DEFAULT) + def _generator(self, compiled_pattern, raw_pattern, maxsize): + vollog.debug(f"RegEx Pattern: {raw_pattern}") layer = self.context.layers[self.config["primary"]] + for offset in layer.scan( - context=self.context, scanner=scanners.RegExScanner(regex_pattern) + context=self.context, scanner=scanners.RegExScanner(raw_pattern) ): result_data = layer.read(offset, maxsize, pad=True) @@ -74,11 +72,37 @@ class RegExScan(plugins.PluginInterface): yield 0, (format_hints.Hex(offset), text_result, bytes_result) def run(self): + pattern = self.config.get("pattern") + + # Handle pattern encoding robustly + if isinstance(pattern, str): + try: + raw_pattern = pattern.encode("utf-8") + except UnicodeEncodeError: + raw_pattern = pattern.encode("latin1", errors="replace") + else: + raw_pattern = pattern + + try: + compiled_pattern = re.compile(raw_pattern) + except re.error as e: + vollog.error(f"Invalid regex pattern: {e}") + return renderers.TreeGrid( + [ + ("Offset", format_hints.Hex), + ("Text", str), + ("Hex", bytes), + ], + [], + ) + + maxsize = self.config.get("maxsize", self.MAXSIZE_DEFAULT) + return renderers.TreeGrid( [ ("Offset", format_hints.Hex), ("Text", str), ("Hex", bytes), ], - self._generator(self.config.get("pattern")), + self._generator(compiled_pattern, raw_pattern, maxsize), ) From 0d7f985ef67cddd57b135bd1454d8f6e0597ab0d Mon Sep 17 00:00:00 2001 From: Mike Auty Date: Sat, 7 Jun 2025 10:16:22 +0100 Subject: [PATCH 112/172] Plugins: Add in __init__ files to create submodules for OS plugins --- volatility3/framework/plugins/linux/graphics/__init__.py | 0 volatility3/framework/plugins/linux/malware/__init__.py | 8 ++++++++ volatility3/framework/plugins/windows/malware/__init__.py | 8 ++++++++ 3 files changed, 16 insertions(+) create mode 100644 volatility3/framework/plugins/linux/graphics/__init__.py create mode 100644 volatility3/framework/plugins/linux/malware/__init__.py create mode 100644 volatility3/framework/plugins/windows/malware/__init__.py diff --git a/volatility3/framework/plugins/linux/graphics/__init__.py b/volatility3/framework/plugins/linux/graphics/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/volatility3/framework/plugins/linux/malware/__init__.py b/volatility3/framework/plugins/linux/malware/__init__.py new file mode 100644 index 000000000..89458befc --- /dev/null +++ b/volatility3/framework/plugins/linux/malware/__init__.py @@ -0,0 +1,8 @@ +# This file is Copyright 2019 Volatility Foundation and licensed under the Volatility Software License 1.0 +# which is available at https://www.volatilityfoundation.org/license/vsl-v1.0 +# +"""All core linux malware plugins. + +These modules should only be imported from volatility3.plugins NOT +volatility3.framework.plugins +""" diff --git a/volatility3/framework/plugins/windows/malware/__init__.py b/volatility3/framework/plugins/windows/malware/__init__.py new file mode 100644 index 000000000..2e2fec739 --- /dev/null +++ b/volatility3/framework/plugins/windows/malware/__init__.py @@ -0,0 +1,8 @@ +# This file is Copyright 2019 Volatility Foundation and licensed under the Volatility Software License 1.0 +# which is available at https://www.volatilityfoundation.org/license/vsl-v1.0 +# +"""All core windows malware plugins. + +These modules should only be imported from volatility3.plugins NOT +volatility3.framework.plugins +""" From fb76bf6b6c6583c0197868780df55190e3158ebf Mon Sep 17 00:00:00 2001 From: Mike Auty Date: Sat, 7 Jun 2025 10:20:59 +0100 Subject: [PATCH 113/172] Linux: Minor linting fixes to fbdev plugin --- .../framework/plugins/linux/graphics/fbdev.py | 25 ++++++++----------- 1 file changed, 10 insertions(+), 15 deletions(-) diff --git a/volatility3/framework/plugins/linux/graphics/fbdev.py b/volatility3/framework/plugins/linux/graphics/fbdev.py index f7cde1bf0..1f9fd74b5 100644 --- a/volatility3/framework/plugins/linux/graphics/fbdev.py +++ b/volatility3/framework/plugins/linux/graphics/fbdev.py @@ -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(), ) From fd1e5510bb5a6e8d3f40b6037fe7bd27a8d2d369 Mon Sep 17 00:00:00 2001 From: SolitudePy <47316655+SolitudePy@users.noreply.github.com> Date: Sat, 7 Jun 2025 12:27:27 +0300 Subject: [PATCH 114/172] categorize malfind as malware plugin --- .../framework/plugins/windows/malfind.py | 293 +----------------- .../plugins/windows/malware/malfind.py | 293 ++++++++++++++++++ 2 files changed, 303 insertions(+), 283 deletions(-) create mode 100644 volatility3/framework/plugins/windows/malware/malfind.py diff --git a/volatility3/framework/plugins/windows/malfind.py b/volatility3/framework/plugins/windows/malfind.py index 33eaf64ef..a98ab5ec0 100644 --- a/volatility3/framework/plugins/windows/malfind.py +++ b/volatility3/framework/plugins/windows/malfind.py @@ -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 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.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[:2] in refined_criteria: - notes = refined_criteria[data[:2]] - - # If we're on a 64 bit kernel, we may still need 32 bit disasm due to wow64 - if is_32bit_arch or proc.get_is_wow64(): - architecture = "intel" - else: - architecture = "intel64" - - disasm = renderers.Disassembly(data, vad.get_start(), architecture) - - file_output = "Disabled" - if self.config["dump"]: - file_output = "Error outputting to file" - try: - file_handle = vadinfo.VadInfo.vad_dump( - self.context, proc, vad, self.open - ) - file_handle.close() - file_output = file_handle.preferred_filename - except (exceptions.InvalidAddressException, OverflowError) as excp: - vollog.debug( - f"Unable to dump PE with pid {proc.UniqueProcessId}.{vad.get_start():#x}: {excp}" - ) - - yield ( - 0, - ( - proc.UniqueProcessId, - process_name, - format_hints.Hex(vad.get_start()), - format_hints.Hex(vad.get_end()), - vad.get_tag(), - vad.get_protection( - vadinfo.VadInfo.protect_values( - self.context, - kernel.layer_name, - kernel.symbol_table_name, - ), - vadinfo.winnt_protections, - ), - vad.get_commit_charge(), - vad.get_private_memory(), - file_output, - notes, - data_object, - disasm, - ), - ) - - def run(self): - filter_func = pslist.PsList.create_pid_filter(self.config.get("pid", None)) - - return renderers.TreeGrid( - [ - ("PID", int), - ("Process", str), - ("Start VPN", format_hints.Hex), - ("End VPN", format_hints.Hex), - ("Tag", str), - ("Protection", str), - ("CommitCharge", int), - ("PrivateMemory", int), - ("File output", str), - ("Notes", str), - ("Hexdump", renderers.LayerData), - ("Disasm", renderers.Disassembly), - ], - self._generator( - pslist.PsList.list_processes( - context=self.context, - kernel_module_name=self.config["kernel"], - filter_func=filter_func, - ) - ), - ) diff --git a/volatility3/framework/plugins/windows/malware/malfind.py b/volatility3/framework/plugins/windows/malware/malfind.py new file mode 100644 index 000000000..33eaf64ef --- /dev/null +++ b/volatility3/framework/plugins/windows/malware/malfind.py @@ -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.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[: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, + ) + ), + ) From 1498926decb332f99bf64620a719c26dc3937db2 Mon Sep 17 00:00:00 2001 From: SolitudePy <47316655+SolitudePy@users.noreply.github.com> Date: Sat, 7 Jun 2025 12:58:31 +0300 Subject: [PATCH 115/172] categorize windows.hollowprocesses as malware --- .../plugins/windows/hollowprocesses.py | 224 +----------------- .../windows/malware/hollowprocesses.py | 223 +++++++++++++++++ 2 files changed, 234 insertions(+), 213 deletions(-) create mode 100644 volatility3/framework/plugins/windows/malware/hollowprocesses.py diff --git a/volatility3/framework/plugins/windows/hollowprocesses.py b/volatility3/framework/plugins/windows/hollowprocesses.py index af559bfbc..9949a223c 100644 --- a/volatility3/framework/plugins/windows/hollowprocesses.py +++ b/volatility3/framework/plugins/windows/hollowprocesses.py @@ -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 = "" - - 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) diff --git a/volatility3/framework/plugins/windows/malware/hollowprocesses.py b/volatility3/framework/plugins/windows/malware/hollowprocesses.py new file mode 100644 index 000000000..f981ae340 --- /dev/null +++ b/volatility3/framework/plugins/windows/malware/hollowprocesses.py @@ -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 = "" + + 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, + ) + ), + ) From 21da9002d7be2492f14cb894b1e742e2efe3e104 Mon Sep 17 00:00:00 2001 From: SolitudePy <47316655+SolitudePy@users.noreply.github.com> Date: Sat, 7 Jun 2025 13:31:26 +0300 Subject: [PATCH 116/172] categorize windows.processghosting as a malware plugin --- .../windows/malware/processghosting.py | 220 +++++++++++++++++ .../plugins/windows/processghosting.py | 222 +----------------- 2 files changed, 231 insertions(+), 211 deletions(-) create mode 100644 volatility3/framework/plugins/windows/malware/processghosting.py diff --git a/volatility3/framework/plugins/windows/malware/processghosting.py b/volatility3/framework/plugins/windows/malware/processghosting.py new file mode 100644 index 000000000..f234bc2e7 --- /dev/null +++ b/volatility3/framework/plugins/windows/malware/processghosting.py @@ -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, + ) + ), + ) diff --git a/volatility3/framework/plugins/windows/processghosting.py b/volatility3/framework/plugins/windows/processghosting.py index f234bc2e7..24eb6fa9a 100644 --- a/volatility3/framework/plugins/windows/processghosting.py +++ b/volatility3/framework/plugins/windows/processghosting.py @@ -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 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, - ) - ), - ) + _version = (1, 0, 0) From e5738c126380a7624702ec4ee95fdb53dc7ed81d Mon Sep 17 00:00:00 2001 From: SolitudePy <47316655+SolitudePy@users.noreply.github.com> Date: Sat, 7 Jun 2025 14:03:36 +0300 Subject: [PATCH 117/172] categorize windows.psxview as malware plugin --- .../plugins/windows/malware/psxview.py | 241 ++++++++++++++++++ .../framework/plugins/windows/psxview.py | 237 +---------------- 2 files changed, 255 insertions(+), 223 deletions(-) create mode 100644 volatility3/framework/plugins/windows/malware/psxview.py diff --git a/volatility3/framework/plugins/windows/malware/psxview.py b/volatility3/framework/plugins/windows/malware/psxview.py new file mode 100644 index 000000000..51616a22c --- /dev/null +++ b/volatility3/framework/plugins/windows/malware/psxview.py @@ -0,0 +1,241 @@ +import datetime +import logging +import string +from itertools import chain +from typing import Dict, Iterable, List + +from volatility3.framework import constants, exceptions, renderers +from volatility3.framework.configuration import requirements +from volatility3.framework.interfaces import plugins +from volatility3.framework.renderers import format_hints +from volatility3.framework.symbols.windows import extensions +from volatility3.plugins.windows import handles, pslist, psscan, thrdscan + +vollog = logging.getLogger(__name__) + + +class PsXView(plugins.PluginInterface): + """Lists all processes found via four of the methods described in \"The Art of Memory Forensics\" which may help \ +identify processes that are trying to hide themselves. + +We recommend using -r pretty if you are looking at this plugin's output in a terminal.""" + + # I've omitted the desktop thread scanning method because Volatility3 doesn't appear to have the functionality + # which the original plugin used to do it. + + # The sessions method is omitted because it begins with the list of processes found by Pslist anyway. + + # Lastly, I've omitted the pspcid method because I could not for the life of me get it to work. I saved the + # code I do have from it, and will happily share it if anyone else wants to add it. + + _required_framework_version = (2, 0, 0) + _version = (1, 0, 0) + + valid_proc_name_chars = set( + string.ascii_lowercase + string.ascii_uppercase + "." + " " + ) + + @classmethod + def get_requirements(cls): + return [ + requirements.ModuleRequirement( + name="kernel", + description="Windows kernel", + architectures=["Intel32", "Intel64"], + ), + requirements.VersionRequirement( + name="pslist", component=pslist.PsList, version=(3, 0, 0) + ), + requirements.VersionRequirement( + name="psscan", component=psscan.PsScan, version=(2, 0, 0) + ), + requirements.VersionRequirement( + name="thrdscan", component=thrdscan.ThrdScan, version=(2, 0, 0) + ), + requirements.VersionRequirement( + name="handles", component=handles.Handles, version=(4, 0, 0) + ), + requirements.BooleanRequirement( + name="physical-offsets", + description="List processes with physical offsets instead of virtual offsets.", + optional=True, + ), + ] + + def _proc_name_to_string(self, proc): + return proc.ImageFileName.cast( + "string", max_length=proc.ImageFileName.vol.count, errors="replace" + ) + + def _is_valid_proc_name(self, string: str) -> bool: + return all(c in self.valid_proc_name_chars for c in string) + + def _filter_garbage_procs( + self, proc_list: Iterable[extensions.EPROCESS] + ) -> List[extensions.EPROCESS]: + return [ + p + for p in proc_list + if p.is_valid() and self._is_valid_proc_name(self._proc_name_to_string(p)) + ] + + def _translate_offset(self, offset: int) -> int: + if not self.config["physical-offsets"]: + return offset + + kernel = self.context.modules[self.config["kernel"]] + layer_name = kernel.layer_name + + try: + _original_offset, _original_length, offset, _length, _layer_name = list( + self.context.layers[layer_name].mapping(offset=offset, length=0) + )[0] + except exceptions.PagedInvalidAddressException: + vollog.debug(f"Page fault: unable to translate {offset:0x}") + + return offset + + def _proc_list_to_dict( + self, tasks: Iterable[extensions.EPROCESS] + ) -> Dict[int, extensions.EPROCESS]: + tasks = self._filter_garbage_procs(tasks) + return {self._translate_offset(proc.vol.offset): proc for proc in tasks} + + def _check_pslist(self, tasks): + return self._proc_list_to_dict(tasks) + + def _check_psscan( + self, + ) -> Dict[int, extensions.EPROCESS]: + res = psscan.PsScan.scan_processes( + context=self.context, kernel_module_name=self.config["kernel"] + ) + + return self._proc_list_to_dict(res) + + def _check_thrdscan(self) -> Dict[int, extensions.EPROCESS]: + ret = [] + + for ethread in thrdscan.ThrdScan.scan_threads( + self.context, module_name="kernel" + ): + process = None + try: + process = ethread.owning_process() + if not process.is_valid(): + continue + + ret.append(process) + except AttributeError: + vollog.log( + constants.LOGLEVEL_VVV, + "Unable to find the owning process of ethread", + ) + + return self._proc_list_to_dict(ret) + + def _check_csrss_handles( + self, tasks: Iterable[extensions.EPROCESS] + ) -> Dict[int, extensions.EPROCESS]: + ret: List[extensions.EPROCESS] = [] + + type_map = handles.Handles.get_type_map( + context=self.context, kernel_module_name=self.config["kernel"] + ) + + cookie = handles.Handles.find_cookie( + context=self.context, kernel_module_name=self.config["kernel"] + ) + + for p in tasks: + name = self._proc_name_to_string(p) + if name != "csrss.exe": + continue + + try: + ret += [ + handle.Body.cast("_EPROCESS") + for handle in handles.Handles.handles( + context=self.context, + kernel_module_name=self.config["kernel"], + handle_table=p.ObjectTable, + ) + if handle.get_object_type(type_map, cookie) == "Process" + ] + except exceptions.InvalidAddressException: + vollog.log( + constants.LOGLEVEL_VVV, "Cannot access eprocess object table" + ) + + return self._proc_list_to_dict(ret) + + def _generator(self): + kdbg_list_processes = list( + pslist.PsList.list_processes( + context=self.context, kernel_module_name=self.config["kernel"] + ) + ) + + # get processes from each source + processes: Dict[str, Dict[int, extensions.EPROCESS]] = {} + + processes["pslist"] = self._check_pslist(kdbg_list_processes) + processes["psscan"] = self._check_psscan() + processes["thrdscan"] = self._check_thrdscan() + processes["csrss"] = self._check_csrss_handles(kdbg_list_processes) + + # Unique set of all offsets from all sources + offsets = set(chain(*(mapping.keys() for mapping in processes.values()))) + + for offset in offsets: + # We know there will be at least one process mapped to each offset + proc: extensions.EPROCESS = next( + mapping[offset] for mapping in processes.values() if offset in mapping + ) + + in_sources = {src: False for src in processes} + + for source, process_mapping in processes.items(): + if offset in process_mapping: + in_sources[source] = True + + pid = proc.UniqueProcessId + name = self._proc_name_to_string(proc) + + exit_time = proc.get_exit_time() + if type(exit_time) is not datetime.datetime: + exit_time = "" + else: + exit_time = str(exit_time) + + yield ( + 0, + ( + format_hints.Hex(offset), + name, + pid, + in_sources["pslist"], + in_sources["psscan"], + in_sources["thrdscan"], + in_sources["csrss"], + exit_time, + ), + ) + + def run(self): + offset_type = "(Physical)" if self.config["physical-offsets"] else "(Virtual)" + offset_str = "Offset" + offset_type + + return renderers.TreeGrid( + [ + (offset_str, format_hints.Hex), + ("Name", str), + ("PID", int), + ("pslist", bool), + ("psscan", bool), + ("thrdscan", bool), + ("csrss", bool), + ("Exit Time", str), + ], + self._generator(), + ) diff --git a/volatility3/framework/plugins/windows/psxview.py b/volatility3/framework/plugins/windows/psxview.py index 51616a22c..625f02387 100644 --- a/volatility3/framework/plugins/windows/psxview.py +++ b/volatility3/framework/plugins/windows/psxview.py @@ -1,24 +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. @@ -30,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=(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(), - ) From a95ebfe3fd2c246e2103c57c61801704ba3e5751 Mon Sep 17 00:00:00 2001 From: SolitudePy <47316655+SolitudePy@users.noreply.github.com> Date: Sat, 7 Jun 2025 16:06:32 +0300 Subject: [PATCH 118/172] categorize windows.suspicious_threads as malware plugin --- .../windows/malware/suspicious_threads.py | 221 ++++++++++++++++++ .../plugins/windows/suspicious_threads.py | 221 +----------------- 2 files changed, 231 insertions(+), 211 deletions(-) create mode 100644 volatility3/framework/plugins/windows/malware/suspicious_threads.py diff --git a/volatility3/framework/plugins/windows/malware/suspicious_threads.py b/volatility3/framework/plugins/windows/malware/suspicious_threads.py new file mode 100644 index 000000000..3da8cb21a --- /dev/null +++ b/volatility3/framework/plugins/windows/malware/suspicious_threads.py @@ -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 = "" + + yield ( + vad_path, + f"This thread started execution in the VAD starting at base address ({vad_base:#x}), which is not backed by a file", + ) + + # All threads should point to PAGE_EXECUTE_WRITECOPY mapped regions + if prot != "PAGE_EXECUTE_WRITECOPY": + yield ( + vad_path, + f"VAD at base address ({vad_base:#x}) hosting this thread has an unexpected starting protection {prot}", + ) + + # check for process hollowing type techniques that mapped in a second, malicious exe file + if ( + exe_path + and vad_path.lower().endswith(".exe") + and (vad_path.lower() != exe_path.lower()) + ): + yield ( + vad_path, + "VAD at base address ({vad_base:#x}) hosting this thread maps an application executable that is not the process executable", + ) + + def _enumerate_processes( + self, kernel: interfaces.context.ModuleInterface, all_ranges + ): + filter_func = pslist.PsList.create_pid_filter(self.config.get("pid", None)) + + for proc in pslist.PsList.list_processes( + context=self.context, + kernel_module_name=self.config["kernel"], + filter_func=filter_func, + ): + ranges = self._get_ranges(kernel, all_ranges, proc) + + # smeared vads or process is terminating + if len(all_ranges[proc.vol.offset]) < 5: + continue + + pid = proc.UniqueProcessId + proc_name = utility.array_to_string(proc.ImageFileName) + + _, __, exe_path = self._get_range(ranges, proc.SectionBaseAddress) + if not isinstance(exe_path, str): + exe_path = None + + yield proc, pid, proc_name, exe_path, ranges + + def _generator(self): + kernel = self.context.modules[self.config["kernel"]] + + all_ranges = {} + + for proc, pid, proc_name, exe_path, ranges in self._enumerate_processes( + kernel, all_ranges + ): + # processes often create multiple threads at the same address + # there is no benefit to checking the same address more than once per process + checked = set() + + for thread in threads.Threads.list_threads( + self.context, self.config["kernel"], proc + ): + # do not process if a thread is exited or terminated (4 = Terminated) + if thread.ExitTime.QuadPart > 0 or thread.Tcb.State == 4: + continue + + # bail if accessing the threads members causes a page fault + info = thrdscan.ThrdScan.gather_thread_info(thread) + if not info: + continue + + _, _, tid, start_address, _, win32_start_address, _, _, _ = info + + addresses = [ + (start_address, "Start"), + (win32_start_address, "Win32Start"), + ] + + for address, context in addresses: + if address in checked: + continue + checked.add(address) + + for vad_path, note in self._check_thread_address( + exe_path, ranges, address + ): + yield 0, ( + proc_name, + pid, + tid, + context, + format_hints.Hex(address), + vad_path, + note, + ) + + def run(self): + return renderers.TreeGrid( + [ + ("Process", str), + ("PID", int), + ("TID", int), + ("Context", str), + ("Address", format_hints.Hex), + ("VAD Path", str), + ("Note", str), + ], + self._generator(), + ) diff --git a/volatility3/framework/plugins/windows/suspicious_threads.py b/volatility3/framework/plugins/windows/suspicious_threads.py index 3da8cb21a..068bdccaf 100644 --- a/volatility3/framework/plugins/windows/suspicious_threads.py +++ b/volatility3/framework/plugins/windows/suspicious_threads.py @@ -1,221 +1,20 @@ -# This file is Copyright 2024 Volatility Foundation and licensed under the Volatility Software License 1.0 +# This file is Copyright 2025 Volatility Foundation and licensed under the Volatility Software License 1.0 # which is available at https://www.volatilityfoundation.org/license/vsl-v1.0 # - import logging -from typing import List, Dict, Tuple, Generator -from volatility3.framework import renderers, interfaces -from volatility3.framework.configuration import requirements -from volatility3.framework.objects import utility -from volatility3.framework.renderers import format_hints -from volatility3.plugins.windows import pslist, threads, vadinfo, thrdscan +from volatility3.framework import interfaces, deprecation +from volatility3.plugins.windows.malware import suspicious_threads vollog = logging.getLogger(__name__) -class SuspiciousThreads(interfaces.plugins.PluginInterface): - """Lists suspicious userland process threads""" +class SuspiciousThreads( + interfaces.plugins.PluginInterface, + deprecation.PluginRenameClass, + replacement_class=suspicious_threads.SuspiciousThreads, + removal_date="2026-06-07", +): + """Lists suspicious userland process threads (deprecated).""" _required_framework_version = (2, 4, 0) _version = (2, 0, 1) - - @classmethod - def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]: - # Since we're calling the plugin, make sure we have the plugin's requirements - return [ - requirements.ModuleRequirement( - name="kernel", - description="Windows kernel", - architectures=["Intel32", "Intel64"], - ), - requirements.ListRequirement( - name="pid", - description="Filter on specific process IDs", - element_type=int, - optional=True, - ), - requirements.VersionRequirement( - name="thrdscan", component=thrdscan.ThrdScan, version=(2, 0, 0) - ), - requirements.VersionRequirement( - name="pslist", component=pslist.PsList, version=(3, 0, 0) - ), - requirements.VersionRequirement( - name="threads", component=threads.Threads, version=(3, 0, 0) - ), - requirements.VersionRequirement( - name="vadinfo", component=vadinfo.VadInfo, version=(2, 0, 0) - ), - ] - - def _get_ranges( - self, - kernel: interfaces.context.ModuleInterface, - all_ranges: Dict[int, List[Tuple[int, int, str, str]]], - proc, - ) -> Tuple[int, int, str, str]: - """ - Maintains a hash table so each process' VADs - are only enumerated once per plugin run - """ - key = proc.vol.offset - - if key not in all_ranges: - all_ranges[key] = [] - - for vad in proc.get_vad_root().traverse(): - fn = vad.get_file_name() - if not isinstance(fn, str) or not fn: - fn = None - - protection_string = vad.get_protection( - vadinfo.VadInfo.protect_values( - self.context, kernel.layer_name, kernel.symbol_table_name - ), - vadinfo.winnt_protections, - ) - - all_ranges[key].append( - (vad.get_start(), vad.get_end(), protection_string, fn) - ) - - return all_ranges[key] - - def _get_range( - self, ranges: Dict[int, List[Tuple[int, int, str, str]]], address: int - ) -> Tuple[int, str, str]: - """ - Walks a process' VADs looking for the one - containing `address` - - Returns its base address, protection string, and mapped file, if any - """ - for start, end, protection_string, fn in ranges: - if start <= address < end: - return start, protection_string, fn - - return None, None, None - - def _check_thread_address( - self, exe_path: str, ranges, thread_address: int - ) -> Generator[Tuple[str, str], None, None]: - vad_base, prot, vad_path = self._get_range(ranges, thread_address) - - # threads outside of a VAD means either smear from this thread or this process' VAD tree - if vad_base is None: - return - - if vad_path is None: - # set this so checks after report the non file backed region in the path column - vad_path = "" - - yield ( - vad_path, - f"This thread started execution in the VAD starting at base address ({vad_base:#x}), which is not backed by a file", - ) - - # All threads should point to PAGE_EXECUTE_WRITECOPY mapped regions - if prot != "PAGE_EXECUTE_WRITECOPY": - yield ( - vad_path, - f"VAD at base address ({vad_base:#x}) hosting this thread has an unexpected starting protection {prot}", - ) - - # check for process hollowing type techniques that mapped in a second, malicious exe file - if ( - exe_path - and vad_path.lower().endswith(".exe") - and (vad_path.lower() != exe_path.lower()) - ): - yield ( - vad_path, - "VAD at base address ({vad_base:#x}) hosting this thread maps an application executable that is not the process executable", - ) - - def _enumerate_processes( - self, kernel: interfaces.context.ModuleInterface, all_ranges - ): - filter_func = pslist.PsList.create_pid_filter(self.config.get("pid", None)) - - for proc in pslist.PsList.list_processes( - context=self.context, - kernel_module_name=self.config["kernel"], - filter_func=filter_func, - ): - ranges = self._get_ranges(kernel, all_ranges, proc) - - # smeared vads or process is terminating - if len(all_ranges[proc.vol.offset]) < 5: - continue - - pid = proc.UniqueProcessId - proc_name = utility.array_to_string(proc.ImageFileName) - - _, __, exe_path = self._get_range(ranges, proc.SectionBaseAddress) - if not isinstance(exe_path, str): - exe_path = None - - yield proc, pid, proc_name, exe_path, ranges - - def _generator(self): - kernel = self.context.modules[self.config["kernel"]] - - all_ranges = {} - - for proc, pid, proc_name, exe_path, ranges in self._enumerate_processes( - kernel, all_ranges - ): - # processes often create multiple threads at the same address - # there is no benefit to checking the same address more than once per process - checked = set() - - for thread in threads.Threads.list_threads( - self.context, self.config["kernel"], proc - ): - # do not process if a thread is exited or terminated (4 = Terminated) - if thread.ExitTime.QuadPart > 0 or thread.Tcb.State == 4: - continue - - # bail if accessing the threads members causes a page fault - info = thrdscan.ThrdScan.gather_thread_info(thread) - if not info: - continue - - _, _, tid, start_address, _, win32_start_address, _, _, _ = info - - addresses = [ - (start_address, "Start"), - (win32_start_address, "Win32Start"), - ] - - for address, context in addresses: - if address in checked: - continue - checked.add(address) - - for vad_path, note in self._check_thread_address( - exe_path, ranges, address - ): - yield 0, ( - proc_name, - pid, - tid, - context, - format_hints.Hex(address), - vad_path, - note, - ) - - def run(self): - return renderers.TreeGrid( - [ - ("Process", str), - ("PID", int), - ("TID", int), - ("Context", str), - ("Address", format_hints.Hex), - ("VAD Path", str), - ("Note", str), - ], - self._generator(), - ) From c85026ee91e7466a394c6475e1c5677fe2eb7d39 Mon Sep 17 00:00:00 2001 From: Mike Auty Date: Sat, 7 Jun 2025 14:20:37 +0100 Subject: [PATCH 119/172] Core: Ensure people running renamed plugins know that the plugins are deprecated --- volatility3/framework/deprecation.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/volatility3/framework/deprecation.py b/volatility3/framework/deprecation.py index 667ea72a5..4866a5a6e 100644 --- a/volatility3/framework/deprecation.py +++ b/volatility3/framework/deprecation.py @@ -133,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) From 520e2cfcdd94c67ebfa8727065880c648d53858f Mon Sep 17 00:00:00 2001 From: Mike Auty Date: Sat, 7 Jun 2025 14:21:22 +0100 Subject: [PATCH 120/172] Core: Fix up warning message --- volatility3/framework/deprecation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/volatility3/framework/deprecation.py b/volatility3/framework/deprecation.py index 4866a5a6e..90fa9bce0 100644 --- a/volatility3/framework/deprecation.py +++ b/volatility3/framework/deprecation.py @@ -139,7 +139,7 @@ class PluginRenameClass: 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.", + 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("__"): From 48a97166ea82a34ff32c39fd301c5a327b523178 Mon Sep 17 00:00:00 2001 From: SolitudePy <47316655+SolitudePy@users.noreply.github.com> Date: Sat, 7 Jun 2025 15:55:20 +0300 Subject: [PATCH 121/172] categorize windows.skeleton_key_check as malware plugin --- .../windows/malware/skeleton_key_check.py | 686 ++++++++++++++++++ .../plugins/windows/skeleton_key_check.py | 685 +---------------- 2 files changed, 696 insertions(+), 675 deletions(-) create mode 100644 volatility3/framework/plugins/windows/malware/skeleton_key_check.py diff --git a/volatility3/framework/plugins/windows/malware/skeleton_key_check.py b/volatility3/framework/plugins/windows/malware/skeleton_key_check.py new file mode 100644 index 000000000..d9cba0704 --- /dev/null +++ b/volatility3/framework/plugins/windows/malware/skeleton_key_check.py @@ -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, + ) + ), + ) diff --git a/volatility3/framework/plugins/windows/skeleton_key_check.py b/volatility3/framework/plugins/windows/skeleton_key_check.py index 6071a2a39..86c5cf1df 100644 --- a/volatility3/framework/plugins/windows/skeleton_key_check.py +++ b/volatility3/framework/plugins/windows/skeleton_key_check.py @@ -1,685 +1,20 @@ -# This file is Copyright 2021 Volatility Foundation and licensed under the Volatility Software License 1.0 +# This file is Copyright 2025 Volatility Foundation and licensed under the Volatility Software License 1.0 # which is available at https://www.volatilityfoundation.org/license/vsl-v1.0 # - -# This module attempts to locate skeleton-key like function hooks. -# It does this by locating the CSystems array through a variety of methods, -# and then validating the entry for RC4 HMAC (0x17 / 23) -# -# For a thorough walkthrough on how the R&D was performed to develop this plugin, -# please see our blogpost here: -# -# https://volatility-labs.blogspot.com/2021/10/memory-forensics-r-illustrated.html - import 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 +from volatility3.framework import interfaces, deprecation +from volatility3.plugins.windows.malware import skeleton_key_check vollog = logging.getLogger(__name__) -class Skeleton_Key_Check(interfaces.plugins.PluginInterface): +class Skeleton_Key_Check( + interfaces.plugins.PluginInterface, + deprecation.PluginRenameClass, + replacement_class=skeleton_key_check.Skeleton_Key_Check, + removal_date="2026-06-07", +): """Looks for signs of Skeleton Key malware""" _required_framework_version = (2, 4, 0) - - @classmethod - def get_requirements(cls): - # Since we're calling the plugin, make sure we have the plugin's requirements - return [ - requirements.ModuleRequirement( - name="kernel", - description="Windows kernel", - architectures=["Intel32", "Intel64"], - ), - requirements.VersionRequirement( - name="pslist", component=pslist.PsList, version=(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, - ) - ), - ) + _version = (1, 0, 0) From 93defa112c707b1155e53f18362c3afc77f3861c Mon Sep 17 00:00:00 2001 From: SolitudePy <47316655+SolitudePy@users.noreply.github.com> Date: Sat, 7 Jun 2025 16:29:30 +0300 Subject: [PATCH 122/172] Plugins: categorize windows.svcdiff as a malware plugin --- .../plugins/windows/malware/svcdiff.py | 102 ++++++++++++++++++ .../framework/plugins/windows/svcdiff.py | 101 ++--------------- 2 files changed, 112 insertions(+), 91 deletions(-) create mode 100644 volatility3/framework/plugins/windows/malware/svcdiff.py diff --git a/volatility3/framework/plugins/windows/malware/svcdiff.py b/volatility3/framework/plugins/windows/malware/svcdiff.py new file mode 100644 index 000000000..78b61eb67 --- /dev/null +++ b/volatility3/framework/plugins/windows/malware/svcdiff.py @@ -0,0 +1,102 @@ +# This file is Copyright 2024 Volatility Foundation and licensed under the Volatility Software License 1.0 +# which is available at https://www.volatilityfoundation.org/license/vsl-v1.0 +# +# This module compares services found through list walking versus scanning, +# with the aim of finding hidden services. +# +# For background of hidden services and a real-world example of the use of this plugin, +# please see our blogpost: +# +# https://volatilityfoundation.org/memory-forensics-rd-illustrated-detecting-hidden-windows-services/ + +import logging + +from volatility3.framework import symbols, interfaces +from volatility3.framework.configuration import requirements +from volatility3.plugins.windows import svclist, svcscan +from volatility3.framework.symbols.windows import versions + +vollog = logging.getLogger(__name__) + + +class SvcDiff(svcscan.SvcScan): + """Compares services found through list walking versus scanning to find rootkits""" + + _required_framework_version = (2, 4, 0) + + _version = (2, 0, 0) + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self._enumeration_method = self.service_diff + + @classmethod + def get_requirements(cls): + # Since we're calling the plugin, make sure we have the plugin's requirements + return [ + requirements.ModuleRequirement( + name="kernel", + description="Windows kernel", + architectures=["Intel32", "Intel64"], + ), + requirements.VersionRequirement( + name="svclist", component=svclist.SvcList, version=(2, 0, 0) + ), + requirements.VersionRequirement( + name="svcscan", component=svcscan.SvcScan, version=(4, 0, 0) + ), + ] + + @classmethod + def service_diff( + cls, + context: interfaces.context.ContextInterface, + kernel_module_name: str, + service_table_name: str, + service_binary_dll_map, + filter_func, + ): + """ + On Windows 10 version 15063+ 64bit Windows memory samples, walk the services list + and scan for services then report differences + """ + kernel = context.modules[kernel_module_name] + + if not symbols.symbol_table_is_64bit( + context=context, symbol_table_name=kernel.symbol_table_name + ) or not versions.is_win10_15063_or_later( + context=context, symbol_table=kernel.symbol_table_name + ): + vollog.warning( + "This plugin only supports Windows 10 version 15063+ 64bit Windows memory samples" + ) + return + + from_scan = set() + from_list = set() + records = {} + + # collect unique service names from scanning + for service in svcscan.SvcScan.service_scan( + context, + kernel_module_name, + service_table_name, + service_binary_dll_map, + filter_func, + ): + from_scan.add(service[6]) + records[service[6]] = service + + # collect services from listing walking + for service in svclist.SvcList.service_list( + context, + kernel_module_name, + service_table_name, + service_binary_dll_map, + filter_func, + ): + from_list.add(service[6]) + + # report services found from scanning but not list walking + for hidden_service in from_scan - from_list: + yield records[hidden_service] diff --git a/volatility3/framework/plugins/windows/svcdiff.py b/volatility3/framework/plugins/windows/svcdiff.py index 78b61eb67..c95a9e62d 100644 --- a/volatility3/framework/plugins/windows/svcdiff.py +++ b/volatility3/framework/plugins/windows/svcdiff.py @@ -1,102 +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 # -# 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 +from volatility3.framework import interfaces, deprecation +from volatility3.plugins.windows.malware import svcdiff vollog = logging.getLogger(__name__) -class SvcDiff(svcscan.SvcScan): - """Compares services found through list walking versus scanning to find rootkits""" +class SvcDiff( + interfaces.plugins.PluginInterface, + deprecation.PluginRenameClass, + replacement_class=svcdiff.SvcDiff, + removal_date="2026-06-07", +): + """Compares services found through list walking versus scanning to find rootkits (deprecated).""" _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] From d4644208a972d8a10b4532c4b0a8a16e5e33e11c Mon Sep 17 00:00:00 2001 From: SolitudePy <47316655+SolitudePy@users.noreply.github.com> Date: Sat, 7 Jun 2025 17:32:54 +0300 Subject: [PATCH 123/172] Plugins: fix svcdiff deprecation wrapper --- volatility3/framework/plugins/windows/svcdiff.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/volatility3/framework/plugins/windows/svcdiff.py b/volatility3/framework/plugins/windows/svcdiff.py index c95a9e62d..bafdf34da 100644 --- a/volatility3/framework/plugins/windows/svcdiff.py +++ b/volatility3/framework/plugins/windows/svcdiff.py @@ -4,18 +4,21 @@ import logging from volatility3.framework import interfaces, deprecation from volatility3.plugins.windows.malware import svcdiff +from volatility3.plugins.windows import svcscan vollog = logging.getLogger(__name__) class SvcDiff( - interfaces.plugins.PluginInterface, + svcscan.SvcScan, deprecation.PluginRenameClass, replacement_class=svcdiff.SvcDiff, removal_date="2026-06-07", ): """Compares services found through list walking versus scanning to find rootkits (deprecated).""" - + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self._enumeration_method = self.service_diff _required_framework_version = (2, 4, 0) _version = (2, 0, 0) From 6537086e62d65f85fcbc3359432237af7278706c Mon Sep 17 00:00:00 2001 From: SolitudePy <47316655+SolitudePy@users.noreply.github.com> Date: Sat, 7 Jun 2025 17:33:09 +0300 Subject: [PATCH 124/172] black --- volatility3/framework/plugins/windows/svcdiff.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/volatility3/framework/plugins/windows/svcdiff.py b/volatility3/framework/plugins/windows/svcdiff.py index bafdf34da..6e4bc30e0 100644 --- a/volatility3/framework/plugins/windows/svcdiff.py +++ b/volatility3/framework/plugins/windows/svcdiff.py @@ -16,9 +16,11 @@ class SvcDiff( removal_date="2026-06-07", ): """Compares services found through list walking versus scanning to find rootkits (deprecated).""" + def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self._enumeration_method = self.service_diff + _required_framework_version = (2, 4, 0) _version = (2, 0, 0) From 45934ae0fd13a88ff03b2325e57424ac56b9eb1d Mon Sep 17 00:00:00 2001 From: SolitudePy <47316655+SolitudePy@users.noreply.github.com> Date: Sat, 7 Jun 2025 17:40:02 +0300 Subject: [PATCH 125/172] removed import for ruff --- volatility3/framework/plugins/windows/svcdiff.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/volatility3/framework/plugins/windows/svcdiff.py b/volatility3/framework/plugins/windows/svcdiff.py index 6e4bc30e0..24bc53e49 100644 --- a/volatility3/framework/plugins/windows/svcdiff.py +++ b/volatility3/framework/plugins/windows/svcdiff.py @@ -2,7 +2,7 @@ # which is available at https://www.volatilityfoundation.org/license/vsl-v1.0 # import logging -from volatility3.framework import interfaces, deprecation +from volatility3.framework import deprecation from volatility3.plugins.windows.malware import svcdiff from volatility3.plugins.windows import svcscan From 66473ac644a89e685f050463f44e04f3a7eb07ab Mon Sep 17 00:00:00 2001 From: SolitudePy <47316655+SolitudePy@users.noreply.github.com> Date: Sat, 7 Jun 2025 18:00:36 +0300 Subject: [PATCH 126/172] categorize windows.unhooked_system_calls as a malware plugin --- .../windows/malware/unhooked_system_calls.py | 202 +++++++++++++++++ .../plugins/windows/unhooked_system_calls.py | 204 +----------------- 2 files changed, 213 insertions(+), 193 deletions(-) create mode 100644 volatility3/framework/plugins/windows/malware/unhooked_system_calls.py diff --git a/volatility3/framework/plugins/windows/malware/unhooked_system_calls.py b/volatility3/framework/plugins/windows/malware/unhooked_system_calls.py new file mode 100644 index 000000000..5723ce0fd --- /dev/null +++ b/volatility3/framework/plugins/windows/malware/unhooked_system_calls.py @@ -0,0 +1,202 @@ +# This file is Copyright 2024 Volatility Foundation and licensed under the Volatility Software License 1.0 +# which is available at https://www.volatilityfoundation.org/license/vsl-v1.0 + +# Full details on the techniques used in these plugins to detect EDR-evading malware +# can be found in our 20 page whitepaper submitted to DEFCON along with the presentation +# https://www.volexity.com/wp-content/uploads/2024/08/Defcon24_EDR_Evasion_Detection_White-Paper_Andrew-Case.pdf + +import logging + +from typing import Dict, Tuple, List, Generator + +from volatility3.framework import interfaces, exceptions +from volatility3.framework import renderers +from volatility3.framework.configuration import requirements +from volatility3.framework.objects import utility +from volatility3.plugins.windows import pslist, pe_symbols + +vollog = logging.getLogger(__name__) + + +class unhooked_system_calls(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: unhooked_system_calls._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=unhooked_system_calls.system_calls, + ) + + # code_bytes[dll_name][func_name][func_bytes] + code_bytes = self._gather_code_bytes(self.config["kernel"], found_symbols) + + # walk the functions that were evaluated + for functions in code_bytes.values(): + # cbb is the distinct groups of bytes (instructions) + # for this function across processes + for func_name, cbb in functions.items(): + # the dict key here is the raw instructions, which is not helpful to look at + # the values are the list of tuples for the (proc_id, proc_name) pairs for this set of bytes (instructions) + cb = list(cbb.values()) + + # if all processes map to the same implementation, then no malware is present + if len(cb) == 1: + yield 0, (func_name, "", len(cb[0])) + else: + # if there are differing implementations then it means + # that malware has overwritten system call(s) in infected processes + # max_idx and small_idx find which implementation of a system call has the least processes + # as all observed malware and open source projects only infected a few targets, leaving the + # rest with the original EDR hooks in place + max_idx = 0 if len(cb[0]) > len(cb[1]) else 1 + small_idx = (~max_idx) & 1 + + ps = [] + + # gather processes on small_idx since these are the malware infected ones + for pid, pname in cb[small_idx]: + ps.append(f"{pid:d}:{pname}") + + proc_names = ", ".join(ps) + + yield 0, (func_name, proc_names, len(cb[max_idx])) + + def run(self) -> renderers.TreeGrid: + return renderers.TreeGrid( + [ + ("Function", str), + ("Distinct Implementations", str), + ("Total Implementations", int), + ], + self._generator(), + ) diff --git a/volatility3/framework/plugins/windows/unhooked_system_calls.py b/volatility3/framework/plugins/windows/unhooked_system_calls.py index 3ff0aa158..0c42415b6 100644 --- a/volatility3/framework/plugins/windows/unhooked_system_calls.py +++ b/volatility3/framework/plugins/windows/unhooked_system_calls.py @@ -1,202 +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 - -# Full details on the techniques used in these plugins to detect EDR-evading malware -# can be found in our 20 page whitepaper submitted to DEFCON along with the presentation -# https://www.volexity.com/wp-content/uploads/2024/08/Defcon24_EDR_Evasion_Detection_White-Paper_Andrew-Case.pdf - +# import logging - -from typing import Dict, Tuple, List, Generator - -from volatility3.framework import interfaces, exceptions -from volatility3.framework import renderers -from volatility3.framework.configuration import requirements -from volatility3.framework.objects import utility -from volatility3.plugins.windows import pslist, pe_symbols +from volatility3.framework import interfaces, deprecation +from volatility3.plugins.windows.malware import unhooked_system_calls vollog = logging.getLogger(__name__) -class unhooked_system_calls(interfaces.plugins.PluginInterface): - """Looks for signs of Skeleton Key malware""" +class unhooked_system_calls( + interfaces.plugins.PluginInterface, + deprecation.PluginRenameClass, + replacement_class=unhooked_system_calls.unhooked_system_calls, + removal_date="2026-06-07", +): + """Detects hooked ntdll.dll stub functions in Windows processes (deprecated).""" _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: unhooked_system_calls._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=unhooked_system_calls.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(), - ) From f97bc920bbfdbded80e9140b09640298a8249481 Mon Sep 17 00:00:00 2001 From: SolitudePy <47316655+SolitudePy@users.noreply.github.com> Date: Sat, 7 Jun 2025 18:09:26 +0300 Subject: [PATCH 127/172] Plugins: changed class name due to incorrect resolution --- .../plugins/windows/malware/unhooked_system_calls.py | 6 +++--- .../framework/plugins/windows/unhooked_system_calls.py | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/volatility3/framework/plugins/windows/malware/unhooked_system_calls.py b/volatility3/framework/plugins/windows/malware/unhooked_system_calls.py index 5723ce0fd..71dc20021 100644 --- a/volatility3/framework/plugins/windows/malware/unhooked_system_calls.py +++ b/volatility3/framework/plugins/windows/malware/unhooked_system_calls.py @@ -18,7 +18,7 @@ from volatility3.plugins.windows import pslist, pe_symbols vollog = logging.getLogger(__name__) -class unhooked_system_calls(interfaces.plugins.PluginInterface): +class UnhookedSystemCalls(interfaces.plugins.PluginInterface): """Detects hooked ntdll.dll stub functions in Windows processes.""" _required_framework_version = (2, 4, 0) @@ -114,7 +114,7 @@ class unhooked_system_calls(interfaces.plugins.PluginInterface): in different processes. This very effectively detects code injection. """ - code_bytes: unhooked_system_calls._code_bytes_type = {} + code_bytes: UnhookedSystemCalls._code_bytes_type = {} procs = pslist.PsList.list_processes(self.context, kernel_module_name) @@ -154,7 +154,7 @@ class unhooked_system_calls(interfaces.plugins.PluginInterface): context=self.context, config_path=self.config_path, kernel_module_name=self.config["kernel"], - symbols=unhooked_system_calls.system_calls, + symbols=UnhookedSystemCalls.system_calls, ) # code_bytes[dll_name][func_name][func_bytes] diff --git a/volatility3/framework/plugins/windows/unhooked_system_calls.py b/volatility3/framework/plugins/windows/unhooked_system_calls.py index 0c42415b6..e6fb2fb6c 100644 --- a/volatility3/framework/plugins/windows/unhooked_system_calls.py +++ b/volatility3/framework/plugins/windows/unhooked_system_calls.py @@ -8,10 +8,10 @@ from volatility3.plugins.windows.malware import unhooked_system_calls vollog = logging.getLogger(__name__) -class unhooked_system_calls( +class UnhookedSystemCalls( interfaces.plugins.PluginInterface, deprecation.PluginRenameClass, - replacement_class=unhooked_system_calls.unhooked_system_calls, + replacement_class=unhooked_system_calls.UnhookedSystemCalls, removal_date="2026-06-07", ): """Detects hooked ntdll.dll stub functions in Windows processes (deprecated).""" From ef07b07659a3c2475b36b6d0c26b5e24ee02687a Mon Sep 17 00:00:00 2001 From: SolitudePy <47316655+SolitudePy@users.noreply.github.com> Date: Sat, 7 Jun 2025 18:25:53 +0300 Subject: [PATCH 128/172] Plugins: categorize windows.drivermodule as a malware plugin --- .../framework/plugins/windows/drivermodule.py | 105 ++---------------- .../plugins/windows/malware/drivermodule.py | 101 +++++++++++++++++ 2 files changed, 113 insertions(+), 93 deletions(-) create mode 100644 volatility3/framework/plugins/windows/malware/drivermodule.py diff --git a/volatility3/framework/plugins/windows/drivermodule.py b/volatility3/framework/plugins/windows/drivermodule.py index c31fe2500..bf8f333f6 100644 --- a/volatility3/framework/plugins/windows/drivermodule.py +++ b/volatility3/framework/plugins/windows/drivermodule.py @@ -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(), - ) diff --git a/volatility3/framework/plugins/windows/malware/drivermodule.py b/volatility3/framework/plugins/windows/malware/drivermodule.py new file mode 100644 index 000000000..c31fe2500 --- /dev/null +++ b/volatility3/framework/plugins/windows/malware/drivermodule.py @@ -0,0 +1,101 @@ +# This file is Copyright 2019 Volatility Foundation and licensed under the Volatility Software License 1.0 +# which is available at https://www.volatilityfoundation.org/license/vsl-v1.0 +# +from typing import Iterator, List, Tuple +from volatility3.framework import renderers, interfaces +from volatility3.framework.configuration import requirements +from volatility3.framework.renderers import format_hints +from volatility3.plugins.windows import ssdt, driverscan, modules + +# built in Windows-components that trigger false positives +KNOWN_DRIVERS = ["ACPI_HAL", "PnpManager", "RAW", "WMIxWDM", "Win32k", "Fs_Rec"] + + +class DriverModule(interfaces.plugins.PluginInterface): + """Determines if any loaded drivers were hidden by a rootkit""" + + _required_framework_version = (2, 0, 0) + _version = (1, 0, 0) + + @classmethod + def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]: + return [ + requirements.ModuleRequirement( + name="kernel", + description="Windows kernel", + architectures=["Intel32", "Intel64"], + ), + requirements.VersionRequirement( + name="ssdt", component=ssdt.SSDT, version=(2, 0, 0) + ), + requirements.VersionRequirement( + name="driverscan", component=driverscan.DriverScan, version=(2, 0, 0) + ), + requirements.VersionRequirement( + name="modules", component=modules.Modules, version=(3, 0, 0) + ), + ] + + def _generator(self) -> Iterator[Tuple]: + """ + Attempt to match each driver's start code address to a known kernel module + A common rootkit technique is to register drivers from modules that are hidden, + which allows us to detect the disconnect between a malicious driver and its hidden module. + """ + collection = ssdt.SSDT.build_module_collection( + context=self.context, + kernel_module_name=self.config["kernel"], + ) + + kernel_space_start = modules.Modules.get_kernel_space_start( + self.context, self.config["kernel"] + ) + + for driver in driverscan.DriverScan.scan_drivers( + self.context, + self.config["kernel"], + ): + # We want starts of 0 as rootkits often set this value + # greater than 0 but less than the kernel space start is smear/terminated though + if 0 < driver.DriverStart < kernel_space_start: + continue + + # we do not care about actual symbol names, we just want to know if the driver points to a known module + module_symbols = list( + collection.get_module_symbols_by_absolute_location(driver.DriverStart) + ) + if not module_symbols: + ( + driver_name, + service_key, + name, + ) = driverscan.DriverScan.get_names_for_driver(driver) + + # drivers without any names will not produce useful output + if not driver_name and not service_key and not name: + continue + + known_exception = driver_name in KNOWN_DRIVERS + + yield ( + 0, + ( + format_hints.Hex(driver.vol.offset), + known_exception, + driver_name or renderers.NotAvailableValue(), + service_key or renderers.NotAvailableValue(), + name or renderers.NotAvailableValue(), + ), + ) + + def run(self) -> renderers.TreeGrid: + return renderers.TreeGrid( + [ + ("Offset", format_hints.Hex), + ("Known Exception", bool), + ("Driver Name", str), + ("Service Key", str), + ("Alternative Name", str), + ], + self._generator(), + ) From 8acf97c475aab174e183669a104e8fa66ef6d599 Mon Sep 17 00:00:00 2001 From: SolitudePy <47316655+SolitudePy@users.noreply.github.com> Date: Sat, 7 Jun 2025 18:42:13 +0300 Subject: [PATCH 129/172] Plugins: categorize linux.check_creds as a malwarep lugin --- .../framework/plugins/linux/check_creds.py | 75 +++---------------- .../plugins/linux/malware/check_creds.py | 71 ++++++++++++++++++ 2 files changed, 83 insertions(+), 63 deletions(-) create mode 100644 volatility3/framework/plugins/linux/malware/check_creds.py diff --git a/volatility3/framework/plugins/linux/check_creds.py b/volatility3/framework/plugins/linux/check_creds.py index e2b84d679..6c2c6f3d5 100644 --- a/volatility3/framework/plugins/linux/check_creds.py +++ b/volatility3/framework/plugins/linux/check_creds.py @@ -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()) diff --git a/volatility3/framework/plugins/linux/malware/check_creds.py b/volatility3/framework/plugins/linux/malware/check_creds.py new file mode 100644 index 000000000..e2b84d679 --- /dev/null +++ b/volatility3/framework/plugins/linux/malware/check_creds.py @@ -0,0 +1,71 @@ +# This file is Copyright 2019 Volatility Foundation and licensed under the Volatility Software License 1.0 +# which is available at https://www.volatilityfoundation.org/license/vsl-v1.0 +# + +from volatility3.framework import interfaces, renderers +from volatility3.framework.renderers import format_hints +from volatility3.framework.configuration import requirements +from volatility3.plugins.linux import pslist + + +class Check_creds(interfaces.plugins.PluginInterface): + """Checks if any processes are sharing credential structures""" + + _required_framework_version = (2, 0, 0) + _version = (2, 0, 2) + + @classmethod + def get_requirements(cls): + return [ + requirements.ModuleRequirement( + name="kernel", + description="Linux kernel", + architectures=["Intel32", "Intel64"], + ), + requirements.VersionRequirement( + name="pslist", component=pslist.PsList, version=(4, 0, 0) + ), + ] + + def _generator(self): + vmlinux = self.context.modules[self.config["kernel"]] + + type_task = vmlinux.get_type("task_struct") + + if not type_task.has_member("cred"): + raise TypeError( + "This plugin requires the task_struct structure to have a cred member. " + "This member is not present in the supplied symbol table. " + "This means you are either analyzing an unsupported kernel version or that your symbol table is corrupt." + ) + + creds = {} + + tasks = pslist.PsList.list_tasks(self.context, vmlinux.name) + + for task in tasks: + task_cred_ptr = task.cred + if not (task_cred_ptr and task_cred_ptr.is_readable()): + continue + + cred_addr = task_cred_ptr.dereference().vol.offset + + creds.setdefault(cred_addr, []) + creds[cred_addr].append(task.pid) + + for cred_addr, pids in creds.items(): + if len(pids) > 1: + pid_str = ", ".join(str(pid) for pid in pids) + + fields = [ + format_hints.Hex(cred_addr), + pid_str, + ] + yield (0, fields) + + def run(self): + headers = [ + ("CredVAddr", format_hints.Hex), + ("PIDs", str), + ] + return renderers.TreeGrid(headers, self._generator()) From 85a5eb5d41ff04b728cacc1cedb4b8a95f4da6cb Mon Sep 17 00:00:00 2001 From: SolitudePy <47316655+SolitudePy@users.noreply.github.com> Date: Sat, 7 Jun 2025 18:42:42 +0300 Subject: [PATCH 130/172] linux.malware.check_creds - fix deps in: test, doc --- doc/source/getting-started-linux-tutorial.rst | 2 +- test/plugins/linux/linux.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/source/getting-started-linux-tutorial.rst b/doc/source/getting-started-linux-tutorial.rst index 031b49636..4c442c938 100644 --- a/doc/source/getting-started-linux-tutorial.rst +++ b/doc/source/getting-started-linux-tutorial.rst @@ -37,7 +37,7 @@ For plugin requests, please create an issue with a description of the requested 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.malware.check_creds.Check_creds linux.check_idt.Check_idt .. note:: Here the command is piped to grep and head to provide the start of the list of linux plugins. diff --git a/test/plugins/linux/linux.py b/test/plugins/linux/linux.py index e39c1d15d..abac11278 100644 --- a/test/plugins/linux/linux.py +++ b/test/plugins/linux/linux.py @@ -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. From c36fdd69f6270b449e3d0f32e9f266cdc06813b2 Mon Sep 17 00:00:00 2001 From: SolitudePy <47316655+SolitudePy@users.noreply.github.com> Date: Sat, 7 Jun 2025 19:22:58 +0300 Subject: [PATCH 131/172] Plugins: categorize linux.check_idt as a malware plugin --- .../framework/plugins/linux/check_idt.py | 166 +---------------- .../plugins/linux/malware/check_idt.py | 168 ++++++++++++++++++ 2 files changed, 177 insertions(+), 157 deletions(-) create mode 100644 volatility3/framework/plugins/linux/malware/check_idt.py diff --git a/volatility3/framework/plugins/linux/check_idt.py b/volatility3/framework/plugins/linux/check_idt.py index e199d98d3..449f85e1e 100644 --- a/volatility3/framework/plugins/linux/check_idt.py +++ b/volatility3/framework/plugins/linux/check_idt.py @@ -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(), - ) diff --git a/volatility3/framework/plugins/linux/malware/check_idt.py b/volatility3/framework/plugins/linux/malware/check_idt.py new file mode 100644 index 000000000..e199d98d3 --- /dev/null +++ b/volatility3/framework/plugins/linux/malware/check_idt.py @@ -0,0 +1,168 @@ +# This file is Copyright 2020 Volatility Foundation and licensed under the Volatility Software License 1.0 +# which is available at https://www.volatilityfoundation.org/license/vsl-v1.0 +# + +import logging +from typing import List, Optional + +import volatility3.framework.symbols.linux.utilities.modules as linux_utilities_modules +from volatility3.framework import interfaces, renderers, symbols +from volatility3.framework.configuration import requirements +from volatility3.framework.renderers import format_hints +from volatility3.framework.symbols import linux + +vollog = logging.getLogger(__name__) + + +class Check_idt(interfaces.plugins.PluginInterface): + """Checks if the IDT has been altered""" + + _required_framework_version = (2, 0, 0) + + # 2.0.0 - Add versioning at all, add `get_idt_type` + _version = (2, 0, 0) + + @classmethod + def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]: + return [ + requirements.ModuleRequirement( + name="kernel", + description="Linux kernel", + architectures=["Intel32", "Intel64"], + ), + requirements.VersionRequirement( + name="linux_utilities_modules", + component=linux_utilities_modules.Modules, + version=(3, 0, 0), + ), + requirements.VersionRequirement( + name="linux_utilities_module_gatherers", + component=linux_utilities_modules.ModuleGatherers, + version=(1, 0, 0), + ), + requirements.VersionRequirement( + name="linuxutils", component=linux.LinuxUtilities, version=(2, 0, 0) + ), + ] + + @staticmethod + def get_idt_type(context, vmlinux_name) -> Optional[str]: + """ + Determines the IDT type for this symbol table or returns None + + The original version ended clauses with an `else` leading to bad fall through + of returning a type that did not exist in the symbol table. + + Future updates should not leave fall through cases to avoid this repeating. + """ + + vmlinux = context.modules[vmlinux_name] + + is_32bit = not symbols.symbol_table_is_64bit(context, vmlinux.symbol_table_name) + + # These are in a specific order. Only append to the lists going forward + # or ask Andrew to run tests before merging. + if is_32bit: + idt_types = ["gate_struct", "desc_struct", "gate_struct32"] + else: + idt_types = ["gate_struct64", "gate_struct", "idt_desc"] + + for idt_type in idt_types: + if vmlinux.has_type(idt_type): + return idt_type + + return None + + def _generator(self): + idt_type = self.get_idt_type(self.context, self.config["kernel"]) + if not idt_type: + vollog.error( + "Unable to determine the data structure type for IDT entries. Please file a bug on the GitHub tracker with your kernel version." + ) + return + + vmlinux = self.context.modules[self.config["kernel"]] + + known_modules = linux_utilities_modules.Modules.run_modules_scanners( + context=self.context, + kernel_module_name=self.config["kernel"], + caller_wanted_gatherers=linux_utilities_modules.ModuleGatherers.all_gatherers_identifier, + ) + + idt_table_size = 256 + + kernel_layer = self.context.layers[vmlinux.layer_name] + + address_mask = kernel_layer.address_mask + + # hw handlers + system call + check_idxs = list(range(20)) + [128] + + addrs = vmlinux.object_from_symbol("idt_table") + + table = vmlinux.object( + object_type="array", + offset=addrs.vol.offset, + subtype=vmlinux.get_type(idt_type), + count=idt_table_size, + absolute=True, + ) + + for i in check_idxs: + ent = table[i] + + if not ent or not kernel_layer.is_valid(ent.vol.offset): + continue + + if hasattr(ent, "a"): + idt_addr = (ent.b & 0xFFFF0000) | (ent.a & 0x0000FFFF) + else: + low = ent.offset_low + middle = ent.offset_middle + + # offset_high is for 64bit systems + if hasattr(ent, "offset_high"): + high = ent.offset_high + else: + high = 0 + + idt_addr = (high << 32) | (middle << 16) | low + + idt_addr = idt_addr & address_mask + + # 0 means unintialized/unused, not a rootkit + if idt_addr == 0: + module_name = renderers.NotAvailableValue() + symbol_name = renderers.NotAvailableValue() + else: + module_info, symbol_name = ( + linux_utilities_modules.Modules.module_lookup_by_address( + self.context, vmlinux.name, known_modules, idt_addr + ) + ) + + if module_info: + module_name = module_info.name + else: + module_name = renderers.NotAvailableValue() + + yield ( + 0, + [ + format_hints.Hex(i), + format_hints.Hex(idt_addr), + module_name, + symbol_name or renderers.NotAvailableValue(), + ], + ) + + def run(self): + return renderers.TreeGrid( + [ + ("Index", format_hints.Hex), + ("Address", format_hints.Hex), + ("Module", str), + ("Symbol", str), + ], + self._generator(), + ) From 4bc1bb818dfb2cb0ca8c2f694ba4f97fdab09208 Mon Sep 17 00:00:00 2001 From: SolitudePy <47316655+SolitudePy@users.noreply.github.com> Date: Sat, 7 Jun 2025 19:23:13 +0300 Subject: [PATCH 132/172] linux.malware.check_idt - fix doc & test deps --- doc/source/getting-started-linux-tutorial.rst | 2 +- test/plugins/linux/linux.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/source/getting-started-linux-tutorial.rst b/doc/source/getting-started-linux-tutorial.rst index 031b49636..9a04ddcc1 100644 --- a/doc/source/getting-started-linux-tutorial.rst +++ b/doc/source/getting-started-linux-tutorial.rst @@ -38,7 +38,7 @@ For plugin requests, please create an issue with a description of the requested linux.bash.Bash Recovers bash command history from memory. linux.check_afinfo.Check_afinfo linux.check_creds.Check_creds - linux.check_idt.Check_idt + linux.malware.check_idt.Check_idt .. note:: Here the command is piped to grep and head to provide the start of the list of linux plugins. diff --git a/test/plugins/linux/linux.py b/test/plugins/linux/linux.py index e39c1d15d..cecf6e80e 100644 --- a/test/plugins/linux/linux.py +++ b/test/plugins/linux/linux.py @@ -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 From 832c997e20b7685f772e518d091fdca534a2e2d8 Mon Sep 17 00:00:00 2001 From: SolitudePy <47316655+SolitudePy@users.noreply.github.com> Date: Sat, 7 Jun 2025 19:29:55 +0300 Subject: [PATCH 133/172] Plugins: categorize linux.check_modules as a malware plugin --- .../framework/plugins/linux/check_modules.py | 68 +++--------------- .../plugins/linux/malware/check_modules.py | 70 +++++++++++++++++++ 2 files changed, 79 insertions(+), 59 deletions(-) create mode 100644 volatility3/framework/plugins/linux/malware/check_modules.py diff --git a/volatility3/framework/plugins/linux/check_modules.py b/volatility3/framework/plugins/linux/check_modules.py index 7805bbd8a..d8b3ddcf1 100644 --- a/volatility3/framework/plugins/linux/check_modules.py +++ b/volatility3/framework/plugins/linux/check_modules.py @@ -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) diff --git a/volatility3/framework/plugins/linux/malware/check_modules.py b/volatility3/framework/plugins/linux/malware/check_modules.py new file mode 100644 index 000000000..7805bbd8a --- /dev/null +++ b/volatility3/framework/plugins/linux/malware/check_modules.py @@ -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) From 4b279f96339f7d5b00af2abf2c8d7d6041777d75 Mon Sep 17 00:00:00 2001 From: SolitudePy <47316655+SolitudePy@users.noreply.github.com> Date: Sat, 7 Jun 2025 19:30:04 +0300 Subject: [PATCH 134/172] linux.malware.check_modules - fix test --- test/plugins/linux/linux.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/plugins/linux/linux.py b/test/plugins/linux/linux.py index e39c1d15d..12309cc59 100644 --- a/test/plugins/linux/linux.py +++ b/test/plugins/linux/linux.py @@ -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. From 5b538d16866e8a173093c5934c493c58f0fb810b Mon Sep 17 00:00:00 2001 From: SolitudePy <47316655+SolitudePy@users.noreply.github.com> Date: Sat, 7 Jun 2025 19:35:19 +0300 Subject: [PATCH 135/172] Plugins: categorize linux.check_syscall as a malware plugin --- .../framework/plugins/linux/check_syscall.py | 216 +----------------- .../plugins/linux/malware/check_syscall.py | 215 +++++++++++++++++ 2 files changed, 226 insertions(+), 205 deletions(-) create mode 100644 volatility3/framework/plugins/linux/malware/check_syscall.py diff --git a/volatility3/framework/plugins/linux/check_syscall.py b/volatility3/framework/plugins/linux/check_syscall.py index 724a67810..5e3e40cbb 100644 --- a/volatility3/framework/plugins/linux/check_syscall.py +++ b/volatility3/framework/plugins/linux/check_syscall.py @@ -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) diff --git a/volatility3/framework/plugins/linux/malware/check_syscall.py b/volatility3/framework/plugins/linux/malware/check_syscall.py new file mode 100644 index 000000000..1188bf250 --- /dev/null +++ b/volatility3/framework/plugins/linux/malware/check_syscall.py @@ -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(), + ) From 70514396b307ca4f4b43bf1587d02cfe7f7dd1e9 Mon Sep 17 00:00:00 2001 From: SolitudePy <47316655+SolitudePy@users.noreply.github.com> Date: Sat, 7 Jun 2025 19:35:26 +0300 Subject: [PATCH 136/172] linux.malware.check_syscall - fix test --- test/plugins/linux/linux.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/plugins/linux/linux.py b/test/plugins/linux/linux.py index e39c1d15d..639cad464 100644 --- a/test/plugins/linux/linux.py +++ b/test/plugins/linux/linux.py @@ -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 From 962665b412980b556d4b003659504a42ca7dda7e Mon Sep 17 00:00:00 2001 From: SolitudePy <47316655+SolitudePy@users.noreply.github.com> Date: Sat, 7 Jun 2025 19:44:50 +0300 Subject: [PATCH 137/172] Plugins: categorize linux.check_afinfo as a malware plugin + test fix --- doc/source/getting-started-linux-tutorial.rst | 2 +- test/plugins/linux/linux.py | 2 +- .../framework/plugins/linux/check_afinfo.py | 215 +----------------- .../plugins/linux/malware/check_afinfo.py | 215 ++++++++++++++++++ 4 files changed, 227 insertions(+), 207 deletions(-) create mode 100644 volatility3/framework/plugins/linux/malware/check_afinfo.py diff --git a/doc/source/getting-started-linux-tutorial.rst b/doc/source/getting-started-linux-tutorial.rst index 031b49636..267d3fc06 100644 --- a/doc/source/getting-started-linux-tutorial.rst +++ b/doc/source/getting-started-linux-tutorial.rst @@ -36,7 +36,7 @@ For plugin requests, please create an issue with a description of the requested $ 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.malware.check_afinfo.Check_afinfo linux.check_creds.Check_creds linux.check_idt.Check_idt diff --git a/test/plugins/linux/linux.py b/test/plugins/linux/linux.py index e39c1d15d..6b4ae1363 100644 --- a/test/plugins/linux/linux.py +++ b/test/plugins/linux/linux.py @@ -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. diff --git a/volatility3/framework/plugins/linux/check_afinfo.py b/volatility3/framework/plugins/linux/check_afinfo.py index 47da21615..3f9e14161 100644 --- a/volatility3/framework/plugins/linux/check_afinfo.py +++ b/volatility3/framework/plugins/linux/check_afinfo.py @@ -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(), - ) diff --git a/volatility3/framework/plugins/linux/malware/check_afinfo.py b/volatility3/framework/plugins/linux/malware/check_afinfo.py new file mode 100644 index 000000000..47da21615 --- /dev/null +++ b/volatility3/framework/plugins/linux/malware/check_afinfo.py @@ -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(), + ) From 1d2b78976a9fe0bbe201dba1df810afe837e8863 Mon Sep 17 00:00:00 2001 From: SolitudePy <47316655+SolitudePy@users.noreply.github.com> Date: Sat, 7 Jun 2025 19:54:44 +0300 Subject: [PATCH 138/172] Plugins: categorize linux.hidden_modules as a malware plugin --- test/plugins/linux/linux.py | 2 +- .../framework/plugins/linux/hidden_modules.py | 196 +---------------- .../plugins/linux/malware/hidden_modules.py | 197 ++++++++++++++++++ 3 files changed, 208 insertions(+), 187 deletions(-) create mode 100644 volatility3/framework/plugins/linux/malware/hidden_modules.py diff --git a/test/plugins/linux/linux.py b/test/plugins/linux/linux.py index 6b4ae1363..63ccc3388 100644 --- a/test/plugins/linux/linux.py +++ b/test/plugins/linux/linux.py @@ -525,7 +525,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. diff --git a/volatility3/framework/plugins/linux/hidden_modules.py b/volatility3/framework/plugins/linux/hidden_modules.py index dcd602c5d..eab3c19a2 100644 --- a/volatility3/framework/plugins/linux/hidden_modules.py +++ b/volatility3/framework/plugins/linux/hidden_modules.py @@ -1,197 +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, 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 diff --git a/volatility3/framework/plugins/linux/malware/hidden_modules.py b/volatility3/framework/plugins/linux/malware/hidden_modules.py new file mode 100644 index 000000000..dcd602c5d --- /dev/null +++ b/volatility3/framework/plugins/linux/malware/hidden_modules.py @@ -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 From 5144f26a8edbe99b90411bfbaa354d4afa1d0b47 Mon Sep 17 00:00:00 2001 From: SolitudePy <47316655+SolitudePy@users.noreply.github.com> Date: Sat, 7 Jun 2025 19:58:12 +0300 Subject: [PATCH 139/172] CI --- volatility3/framework/plugins/linux/hidden_modules.py | 1 - 1 file changed, 1 deletion(-) diff --git a/volatility3/framework/plugins/linux/hidden_modules.py b/volatility3/framework/plugins/linux/hidden_modules.py index eab3c19a2..f7bdd6b0b 100644 --- a/volatility3/framework/plugins/linux/hidden_modules.py +++ b/volatility3/framework/plugins/linux/hidden_modules.py @@ -18,4 +18,3 @@ class Hidden_modules( _required_framework_version = (2, 25, 0) _version = (3, 0, 2) - From 35c4e9f50b3b7dc09af0bb0c7bccbe884bf819db Mon Sep 17 00:00:00 2001 From: SolitudePy <47316655+SolitudePy@users.noreply.github.com> Date: Sat, 7 Jun 2025 21:52:22 +0300 Subject: [PATCH 140/172] Plugins: change class name to original --- volatility3/framework/plugins/windows/unhooked_system_calls.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/volatility3/framework/plugins/windows/unhooked_system_calls.py b/volatility3/framework/plugins/windows/unhooked_system_calls.py index e6fb2fb6c..a0864e38d 100644 --- a/volatility3/framework/plugins/windows/unhooked_system_calls.py +++ b/volatility3/framework/plugins/windows/unhooked_system_calls.py @@ -8,7 +8,7 @@ from volatility3.plugins.windows.malware import unhooked_system_calls vollog = logging.getLogger(__name__) -class UnhookedSystemCalls( +class unhooked_system_calls( interfaces.plugins.PluginInterface, deprecation.PluginRenameClass, replacement_class=unhooked_system_calls.UnhookedSystemCalls, From 21d66839fe942aeb327572f7056850ea813d396d Mon Sep 17 00:00:00 2001 From: SolitudePy <47316655+SolitudePy@users.noreply.github.com> Date: Sat, 7 Jun 2025 21:56:36 +0300 Subject: [PATCH 141/172] use class name with module name? --- .../framework/plugins/windows/unhooked_system_calls.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/volatility3/framework/plugins/windows/unhooked_system_calls.py b/volatility3/framework/plugins/windows/unhooked_system_calls.py index a0864e38d..fa88f5523 100644 --- a/volatility3/framework/plugins/windows/unhooked_system_calls.py +++ b/volatility3/framework/plugins/windows/unhooked_system_calls.py @@ -3,7 +3,7 @@ # import logging from volatility3.framework import interfaces, deprecation -from volatility3.plugins.windows.malware import unhooked_system_calls +from volatility3.plugins.windows import malware vollog = logging.getLogger(__name__) @@ -11,7 +11,7 @@ vollog = logging.getLogger(__name__) class unhooked_system_calls( interfaces.plugins.PluginInterface, deprecation.PluginRenameClass, - replacement_class=unhooked_system_calls.UnhookedSystemCalls, + replacement_class=malware.unhooked_system_calls.UnhookedSystemCalls, removal_date="2026-06-07", ): """Detects hooked ntdll.dll stub functions in Windows processes (deprecated).""" From c71fd30c8a4baa873b27044546822b7e9084ad62 Mon Sep 17 00:00:00 2001 From: SolitudePy <47316655+SolitudePy@users.noreply.github.com> Date: Sat, 7 Jun 2025 21:58:43 +0300 Subject: [PATCH 142/172] shortcut class name --- .../framework/plugins/windows/unhooked_system_calls.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/volatility3/framework/plugins/windows/unhooked_system_calls.py b/volatility3/framework/plugins/windows/unhooked_system_calls.py index fa88f5523..70219da13 100644 --- a/volatility3/framework/plugins/windows/unhooked_system_calls.py +++ b/volatility3/framework/plugins/windows/unhooked_system_calls.py @@ -3,7 +3,7 @@ # import logging from volatility3.framework import interfaces, deprecation -from volatility3.plugins.windows import malware +from volatility3.plugins.windows.malware import unhooked_system_calls as unhooked_syscalls vollog = logging.getLogger(__name__) @@ -11,7 +11,7 @@ vollog = logging.getLogger(__name__) class unhooked_system_calls( interfaces.plugins.PluginInterface, deprecation.PluginRenameClass, - replacement_class=malware.unhooked_system_calls.UnhookedSystemCalls, + replacement_class=unhooked_syscalls.UnhookedSystemCalls, removal_date="2026-06-07", ): """Detects hooked ntdll.dll stub functions in Windows processes (deprecated).""" From f9941b5b6ec67d79da687870d727c8ea082cd3e5 Mon Sep 17 00:00:00 2001 From: SolitudePy <47316655+SolitudePy@users.noreply.github.com> Date: Sat, 7 Jun 2025 22:00:03 +0300 Subject: [PATCH 143/172] black --- .../framework/plugins/windows/unhooked_system_calls.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/volatility3/framework/plugins/windows/unhooked_system_calls.py b/volatility3/framework/plugins/windows/unhooked_system_calls.py index 70219da13..3827bbe6e 100644 --- a/volatility3/framework/plugins/windows/unhooked_system_calls.py +++ b/volatility3/framework/plugins/windows/unhooked_system_calls.py @@ -3,7 +3,9 @@ # import logging from volatility3.framework import interfaces, deprecation -from volatility3.plugins.windows.malware import unhooked_system_calls as unhooked_syscalls +from volatility3.plugins.windows.malware import ( + unhooked_system_calls as unhooked_syscalls, +) vollog = logging.getLogger(__name__) From faf7d781be8f7b15d15d28126676dc7d11b2e12e Mon Sep 17 00:00:00 2001 From: SolitudePy <47316655+SolitudePy@users.noreply.github.com> Date: Sat, 7 Jun 2025 22:55:29 +0300 Subject: [PATCH 144/172] Plugins: categorize linux.keyboard_notifiers as a malware plugin --- test/plugins/linux/linux.py | 2 +- .../plugins/linux/keyboard_notifiers.py | 106 ++---------------- .../linux/malware/keyboard_notifiers.py | 105 +++++++++++++++++ 3 files changed, 117 insertions(+), 96 deletions(-) create mode 100644 volatility3/framework/plugins/linux/malware/keyboard_notifiers.py diff --git a/test/plugins/linux/linux.py b/test/plugins/linux/linux.py index e39c1d15d..9a11d9c95 100644 --- a/test/plugins/linux/linux.py +++ b/test/plugins/linux/linux.py @@ -479,7 +479,7 @@ 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. diff --git a/volatility3/framework/plugins/linux/keyboard_notifiers.py b/volatility3/framework/plugins/linux/keyboard_notifiers.py index 215704350..72dcc7bad 100644 --- a/volatility3/framework/plugins/linux/keyboard_notifiers.py +++ b/volatility3/framework/plugins/linux/keyboard_notifiers.py @@ -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) diff --git a/volatility3/framework/plugins/linux/malware/keyboard_notifiers.py b/volatility3/framework/plugins/linux/malware/keyboard_notifiers.py new file mode 100644 index 000000000..9e99809b2 --- /dev/null +++ b/volatility3/framework/plugins/linux/malware/keyboard_notifiers.py @@ -0,0 +1,105 @@ +# This file is Copyright 2020 Volatility Foundation and licensed under the Volatility Software License 1.0 +# which is available at https://www.volatilityfoundation.org/license/vsl-v1.0 +# + +import logging + +import volatility3.framework.symbols.linux.utilities.modules as linux_utilities_modules +from volatility3.framework import interfaces, renderers, exceptions +from volatility3.framework.configuration import requirements +from volatility3.framework.renderers import format_hints +from volatility3.framework.symbols import linux + +vollog = logging.getLogger(__name__) + + +class Keyboard_notifiers(interfaces.plugins.PluginInterface): + """Parses the keyboard notifier call chain""" + + _required_framework_version = (2, 0, 0) + _version = (1, 0, 0) + + @classmethod + def get_requirements(cls): + return [ + requirements.ModuleRequirement( + name="kernel", + description="Linux kernel", + architectures=["Intel32", "Intel64"], + ), + requirements.VersionRequirement( + name="linux_utilities_modules", + component=linux_utilities_modules.Modules, + version=(3, 0, 0), + ), + requirements.VersionRequirement( + name="linux_utilities_module_gatherers", + component=linux_utilities_modules.ModuleGatherers, + version=(1, 0, 0), + ), + requirements.VersionRequirement( + name="linuxutils", component=linux.LinuxUtilities, version=(2, 0, 0) + ), + ] + + def _generator(self): + vmlinux = self.context.modules[self.config["kernel"]] + + try: + knl_addr = vmlinux.object_from_symbol("keyboard_notifier_list") + except exceptions.SymbolError: + knl_addr = None + + if not knl_addr: + raise TypeError( + "This plugin requires the keyboard_notifier_list structure. " + "This structure is not present in the supplied symbol table. " + "This means you are either analyzing an unsupported kernel version or that your symbol table is corrupt." + ) + + if not self.context.layers[vmlinux.layer_name].is_valid(knl_addr.vol.offset): + vollog.error("The head of the keyboard notifier list is paged out.") + return + + known_modules = linux_utilities_modules.Modules.run_modules_scanners( + context=self.context, + kernel_module_name=self.config["kernel"], + caller_wanted_gatherers=linux_utilities_modules.ModuleGatherers.all_gatherers_identifier, + ) + + knl = vmlinux.object( + object_type="atomic_notifier_head", + offset=knl_addr.vol.offset, + absolute=True, + ) + + for call_back in linux.LinuxUtilities.walk_internal_list( + vmlinux, "notifier_block", "next", knl.head + ): + call_addr = call_back.notifier_call + + module_info, symbol_name = ( + linux_utilities_modules.Modules.module_lookup_by_address( + self.context, vmlinux.name, known_modules, call_addr + ) + ) + + if module_info: + module_name = module_info.name + else: + module_name = renderers.NotAvailableValue() + + yield ( + 0, + [ + format_hints.Hex(call_addr), + module_name, + symbol_name or renderers.NotAvailableValue(), + ], + ) + + def run(self): + return renderers.TreeGrid( + [("Address", format_hints.Hex), ("Module", str), ("Symbol", str)], + self._generator(), + ) From a06d59bc52d670ba3ba9ccef11c0fdd9d4e88d0c Mon Sep 17 00:00:00 2001 From: SolitudePy <47316655+SolitudePy@users.noreply.github.com> Date: Sat, 7 Jun 2025 22:58:46 +0300 Subject: [PATCH 145/172] Plugins: categorize linux.malfind as a malware plugin --- test/plugins/linux/linux.py | 7 +- .../framework/plugins/linux/malfind.py | 114 ++---------------- .../plugins/linux/malware/malfind.py | 114 ++++++++++++++++++ 3 files changed, 129 insertions(+), 106 deletions(-) create mode 100644 volatility3/framework/plugins/linux/malware/malfind.py diff --git a/test/plugins/linux/linux.py b/test/plugins/linux/linux.py index 9a11d9c95..69aeb8a6c 100644 --- a/test/plugins/linux/linux.py +++ b/test/plugins/linux/linux.py @@ -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. @@ -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.malware.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. diff --git a/volatility3/framework/plugins/linux/malfind.py b/volatility3/framework/plugins/linux/malfind.py index c7e141c02..647e1531a 100644 --- a/volatility3/framework/plugins/linux/malfind.py +++ b/volatility3/framework/plugins/linux/malfind.py @@ -1,114 +1,20 @@ -# This file is Copyright 2019 Volatility Foundation and licensed under the Volatility Software License 1.0 +# This file is Copyright 2025 Volatility Foundation and licensed under the Volatility Software License 1.0 # which is available at https://www.volatilityfoundation.org/license/vsl-v1.0 # - -from typing import List, 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 - ) - ), - ) diff --git a/volatility3/framework/plugins/linux/malware/malfind.py b/volatility3/framework/plugins/linux/malware/malfind.py new file mode 100644 index 000000000..c7e141c02 --- /dev/null +++ b/volatility3/framework/plugins/linux/malware/malfind.py @@ -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 + ) + ), + ) From 6c35bc3fa0135c7c4124b327d536f7ea4cba11ca Mon Sep 17 00:00:00 2001 From: SolitudePy <47316655+SolitudePy@users.noreply.github.com> Date: Tue, 10 Jun 2025 19:12:09 +0300 Subject: [PATCH 146/172] Plugins: categorize linux.modxview as a malware plugin --- .../plugins/linux/malware/modxview.py | 181 ++++++++++++++++++ .../framework/plugins/linux/modxview.py | 181 ++---------------- 2 files changed, 192 insertions(+), 170 deletions(-) create mode 100644 volatility3/framework/plugins/linux/malware/modxview.py diff --git a/volatility3/framework/plugins/linux/malware/modxview.py b/volatility3/framework/plugins/linux/malware/modxview.py new file mode 100644 index 000000000..c1707d26f --- /dev/null +++ b/volatility3/framework/plugins/linux/malware/modxview.py @@ -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(), + ) diff --git a/volatility3/framework/plugins/linux/modxview.py b/volatility3/framework/plugins/linux/modxview.py index c1707d26f..f710b1291 100644 --- a/volatility3/framework/plugins/linux/modxview.py +++ b/volatility3/framework/plugins/linux/modxview.py @@ -1,181 +1,22 @@ -# This file is Copyright 2024 Volatility Foundation and licensed under the Volatility Software License 1.0 +# This file is Copyright 2025 Volatility Foundation and licensed under the Volatility Software License 1.0 # which is available at https://www.volatilityfoundation.org/license/vsl-v1.0 # 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(), - ) From 77801e4cb09ce54ca576bc2ee11ad009d6ce42fa Mon Sep 17 00:00:00 2001 From: SolitudePy <47316655+SolitudePy@users.noreply.github.com> Date: Tue, 10 Jun 2025 19:22:12 +0300 Subject: [PATCH 147/172] Plugins: categorize linux.netfilter as a malware plugin --- test/plugins/linux/linux.py | 2 +- .../plugins/linux/malware/netfilter.py | 806 +++++++++++++++++ .../framework/plugins/linux/modxview.py | 1 - .../framework/plugins/linux/netfilter.py | 808 +----------------- 4 files changed, 818 insertions(+), 799 deletions(-) create mode 100644 volatility3/framework/plugins/linux/malware/netfilter.py diff --git a/test/plugins/linux/linux.py b/test/plugins/linux/linux.py index e39c1d15d..c7f1df9c6 100644 --- a/test/plugins/linux/linux.py +++ b/test/plugins/linux/linux.py @@ -501,7 +501,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. diff --git a/volatility3/framework/plugins/linux/malware/netfilter.py b/volatility3/framework/plugins/linux/malware/netfilter.py new file mode 100644 index 000000000..d724d4296 --- /dev/null +++ b/volatility3/framework/plugins/linux/malware/netfilter.py @@ -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()) diff --git a/volatility3/framework/plugins/linux/modxview.py b/volatility3/framework/plugins/linux/modxview.py index f710b1291..d91f37587 100644 --- a/volatility3/framework/plugins/linux/modxview.py +++ b/volatility3/framework/plugins/linux/modxview.py @@ -14,7 +14,6 @@ class Modxview( replacement_class=modxview.Modxview, removal_date="2026-06-07", ): - """Centralize lsmod, check_modules and hidden_modules results to efficiently \ spot modules presence and taints (deprecated).""" diff --git a/volatility3/framework/plugins/linux/netfilter.py b/volatility3/framework/plugins/linux/netfilter.py index d724d4296..741241039 100644 --- a/volatility3/framework/plugins/linux/netfilter.py +++ b/volatility3/framework/plugins/linux/netfilter.py @@ -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 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) +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) From e3877f68ec463a8f39b946014214e5cbaf8a52b5 Mon Sep 17 00:00:00 2001 From: SolitudePy <47316655+SolitudePy@users.noreply.github.com> Date: Tue, 10 Jun 2025 19:37:57 +0300 Subject: [PATCH 148/172] Plugins: categorize linux.tty_check as a malware plugin --- test/plugins/linux/linux.py | 2 +- .../plugins/linux/malware/tty_check.py | 119 +++++++++++++++++ .../framework/plugins/linux/tty_check.py | 120 ++---------------- 3 files changed, 131 insertions(+), 110 deletions(-) create mode 100644 volatility3/framework/plugins/linux/malware/tty_check.py diff --git a/test/plugins/linux/linux.py b/test/plugins/linux/linux.py index c7f1df9c6..99fb4136a 100644 --- a/test/plugins/linux/linux.py +++ b/test/plugins/linux/linux.py @@ -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 diff --git a/volatility3/framework/plugins/linux/malware/tty_check.py b/volatility3/framework/plugins/linux/malware/tty_check.py new file mode 100644 index 000000000..1547c5cc6 --- /dev/null +++ b/volatility3/framework/plugins/linux/malware/tty_check.py @@ -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(), + ) diff --git a/volatility3/framework/plugins/linux/tty_check.py b/volatility3/framework/plugins/linux/tty_check.py index 7d30b84ee..36bfb1b5a 100644 --- a/volatility3/framework/plugins/linux/tty_check.py +++ b/volatility3/framework/plugins/linux/tty_check.py @@ -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) From 8040c049e0c338e66a4f70671b6976421d2ae361 Mon Sep 17 00:00:00 2001 From: SolitudePy <47316655+SolitudePy@users.noreply.github.com> Date: Tue, 10 Jun 2025 20:30:05 +0300 Subject: [PATCH 149/172] Tests: change class name for tty_check --- test/plugins/linux/linux.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/plugins/linux/linux.py b/test/plugins/linux/linux.py index 99fb4136a..d6fbf5fa8 100644 --- a/test/plugins/linux/linux.py +++ b/test/plugins/linux/linux.py @@ -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.malware.tty_check.tty_check", image, volatility, python + "linux.malware.tty_check.Tty_Check", image, volatility, python ) assert rc == 0 From 742b0634b931855a74f39fdb3e9ebbe8142ee7db Mon Sep 17 00:00:00 2001 From: SolitudePy <47316655+SolitudePy@users.noreply.github.com> Date: Wed, 11 Jun 2025 19:30:29 +0300 Subject: [PATCH 150/172] readjust _run to contain less code & removal of redundant code --- volatility3/framework/plugins/regexscan.py | 43 ++++++++-------------- 1 file changed, 15 insertions(+), 28 deletions(-) diff --git a/volatility3/framework/plugins/regexscan.py b/volatility3/framework/plugins/regexscan.py index e95986899..1f199658e 100644 --- a/volatility3/framework/plugins/regexscan.py +++ b/volatility3/framework/plugins/regexscan.py @@ -46,12 +46,21 @@ class RegExScan(plugins.PluginInterface): ), ] - def _generator(self, compiled_pattern, raw_pattern, maxsize): - vollog.debug(f"RegEx Pattern: {raw_pattern}") - layer = self.context.layers[self.config["primary"]] + def _generator(self, layer, pattern, maxsize): + vollog.debug(f"RegEx Pattern: {pattern}") + + # Convert string pattern to bytes for RegExScanner + pattern_bytes = pattern.encode("utf-8") + + # Compile the pattern here to ensure consistency + try: + compiled_pattern = re.compile(pattern_bytes) + except re.error as e: + vollog.error(f"Invalid regex pattern: {e}") + raise ValueError(f"Invalid regex pattern: {e}") for offset in layer.scan( - context=self.context, scanner=scanners.RegExScanner(raw_pattern) + context=self.context, scanner=scanners.RegExScanner(pattern_bytes) ): result_data = layer.read(offset, maxsize, pad=True) @@ -73,30 +82,8 @@ class RegExScan(plugins.PluginInterface): def run(self): pattern = self.config.get("pattern") - - # Handle pattern encoding robustly - if isinstance(pattern, str): - try: - raw_pattern = pattern.encode("utf-8") - except UnicodeEncodeError: - raw_pattern = pattern.encode("latin1", errors="replace") - else: - raw_pattern = pattern - - try: - compiled_pattern = re.compile(raw_pattern) - except re.error as e: - vollog.error(f"Invalid regex pattern: {e}") - return renderers.TreeGrid( - [ - ("Offset", format_hints.Hex), - ("Text", str), - ("Hex", bytes), - ], - [], - ) - maxsize = self.config.get("maxsize", self.MAXSIZE_DEFAULT) + layer = self.context.layers[self.config["primary"]] return renderers.TreeGrid( [ @@ -104,5 +91,5 @@ class RegExScan(plugins.PluginInterface): ("Text", str), ("Hex", bytes), ], - self._generator(compiled_pattern, raw_pattern, maxsize), + self._generator(layer, pattern, maxsize), ) From c270cf4b15123662c2ca2d0af48bc5144fafd78e Mon Sep 17 00:00:00 2001 From: SolitudePy <47316655+SolitudePy@users.noreply.github.com> Date: Thu, 12 Jun 2025 20:09:28 +0300 Subject: [PATCH 151/172] RegexScan: parameterize _generator --- volatility3/framework/plugins/regexscan.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/volatility3/framework/plugins/regexscan.py b/volatility3/framework/plugins/regexscan.py index 1f199658e..2c188ba4c 100644 --- a/volatility3/framework/plugins/regexscan.py +++ b/volatility3/framework/plugins/regexscan.py @@ -46,7 +46,8 @@ class RegExScan(plugins.PluginInterface): ), ] - def _generator(self, layer, pattern, maxsize): + 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 @@ -60,7 +61,7 @@ class RegExScan(plugins.PluginInterface): raise ValueError(f"Invalid regex pattern: {e}") for offset in layer.scan( - context=self.context, scanner=scanners.RegExScanner(pattern_bytes) + context=context, scanner=scanners.RegExScanner(pattern_bytes) ): result_data = layer.read(offset, maxsize, pad=True) @@ -83,7 +84,8 @@ class RegExScan(plugins.PluginInterface): def run(self): pattern = self.config.get("pattern") maxsize = self.config.get("maxsize", self.MAXSIZE_DEFAULT) - layer = self.context.layers[self.config["primary"]] + layer_name = self.config["primary"] + context = self.context return renderers.TreeGrid( [ @@ -91,5 +93,5 @@ class RegExScan(plugins.PluginInterface): ("Text", str), ("Hex", bytes), ], - self._generator(layer, pattern, maxsize), + self._generator(context, layer_name, pattern, maxsize), ) From 215ba1dfaad2b32f9d07b2d615ee6b92166be8be Mon Sep 17 00:00:00 2001 From: SolitudePy <47316655+SolitudePy@users.noreply.github.com> Date: Fri, 13 Jun 2025 17:42:17 +0300 Subject: [PATCH 152/172] Plugins: categorize ldrmodules as a malware plugin --- test/plugins/windows/windows.py | 26 ++-- .../framework/plugins/windows/ldrmodules.py | 126 ++--------------- .../plugins/windows/malware/ldrmodules.py | 128 ++++++++++++++++++ 3 files changed, 151 insertions(+), 129 deletions(-) create mode 100644 volatility3/framework/plugins/windows/malware/ldrmodules.py diff --git a/test/plugins/windows/windows.py b/test/plugins/windows/windows.py index 0f44ba533..431461b6d 100644 --- a/test/plugins/windows/windows.py +++ b/test/plugins/windows/windows.py @@ -58,6 +58,7 @@ 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 @@ -67,6 +68,7 @@ class TestWindowsTimeliner: 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 @@ -780,19 +782,19 @@ class TestWindowsSymlinkScan: 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": "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": [] - } + "CreateTime": "2005-06-25T16:47:28+00:00", + "From Name": "UNC", + "Offset": 453176664, + "To Name": "\\Device\\Mup", + "__children": [], + }, ] for expected_row in expected_rows: @@ -803,7 +805,7 @@ 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, diff --git a/volatility3/framework/plugins/windows/ldrmodules.py b/volatility3/framework/plugins/windows/ldrmodules.py index 32432c44e..efb62f8f6 100644 --- a/volatility3/framework/plugins/windows/ldrmodules.py +++ b/volatility3/framework/plugins/windows/ldrmodules.py @@ -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, - ) - ), - ) diff --git a/volatility3/framework/plugins/windows/malware/ldrmodules.py b/volatility3/framework/plugins/windows/malware/ldrmodules.py new file mode 100644 index 000000000..32432c44e --- /dev/null +++ b/volatility3/framework/plugins/windows/malware/ldrmodules.py @@ -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, + ) + ), + ) From e37ed0e806d1028a4766289626551fd8fac4c099 Mon Sep 17 00:00:00 2001 From: SolitudePy <47316655+SolitudePy@users.noreply.github.com> Date: Sat, 14 Jun 2025 23:08:15 +0300 Subject: [PATCH 153/172] Plugins: categorize direct_system_calls as a malware plugin --- .../plugins/windows/direct_system_calls.py | 432 +--------------- .../windows/malware/direct_system_calls.py | 472 ++++++++++++++++++ 2 files changed, 483 insertions(+), 421 deletions(-) create mode 100644 volatility3/framework/plugins/windows/malware/direct_system_calls.py diff --git a/volatility3/framework/plugins/windows/direct_system_calls.py b/volatility3/framework/plugins/windows/direct_system_calls.py index dce09605b..9616696d8 100644 --- a/volatility3/framework/plugins/windows/direct_system_calls.py +++ b/volatility3/framework/plugins/windows/direct_system_calls.py @@ -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,13 @@ 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' 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""" +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) @@ -80,393 +59,4 @@ class DirectSystemCalls(interfaces.plugins.PluginInterface): ["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(), - ) + ) \ No newline at end of file diff --git a/volatility3/framework/plugins/windows/malware/direct_system_calls.py b/volatility3/framework/plugins/windows/malware/direct_system_calls.py new file mode 100644 index 000000000..dce09605b --- /dev/null +++ b/volatility3/framework/plugins/windows/malware/direct_system_calls.py @@ -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, + # 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(), + ) From 7cc8c6e94c3a6f3eccf318afd0b46ee46ec011d9 Mon Sep 17 00:00:00 2001 From: SolitudePy <47316655+SolitudePy@users.noreply.github.com> Date: Sat, 14 Jun 2025 23:08:56 +0300 Subject: [PATCH 154/172] Plugins: categorize indirect_system_calls as a malware plugin --- .../plugins/windows/indirect_system_calls.py | 122 ++---------------- .../windows/malware/indirect_system_calls.py | 120 +++++++++++++++++ 2 files changed, 132 insertions(+), 110 deletions(-) create mode 100644 volatility3/framework/plugins/windows/malware/indirect_system_calls.py diff --git a/volatility3/framework/plugins/windows/indirect_system_calls.py b/volatility3/framework/plugins/windows/indirect_system_calls.py index 26216d2c3..65f5f8734 100644 --- a/volatility3/framework/plugins/windows/indirect_system_calls.py +++ b/volatility3/framework/plugins/windows/indirect_system_calls.py @@ -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(" 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(" Date: Sat, 14 Jun 2025 23:15:01 +0300 Subject: [PATCH 155/172] black --- .../framework/plugins/windows/malware/indirect_system_calls.py | 1 + 1 file changed, 1 insertion(+) diff --git a/volatility3/framework/plugins/windows/malware/indirect_system_calls.py b/volatility3/framework/plugins/windows/malware/indirect_system_calls.py index cb4565086..ba34eb110 100644 --- a/volatility3/framework/plugins/windows/malware/indirect_system_calls.py +++ b/volatility3/framework/plugins/windows/malware/indirect_system_calls.py @@ -16,6 +16,7 @@ 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) From aa4ef88b51a72324ffad0976064ee0c1f6941195 Mon Sep 17 00:00:00 2001 From: SolitudePy <47316655+SolitudePy@users.noreply.github.com> Date: Sat, 14 Jun 2025 23:17:04 +0300 Subject: [PATCH 156/172] fix: black lint --- volatility3/framework/plugins/windows/direct_system_calls.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/volatility3/framework/plugins/windows/direct_system_calls.py b/volatility3/framework/plugins/windows/direct_system_calls.py index 9616696d8..79d02fe67 100644 --- a/volatility3/framework/plugins/windows/direct_system_calls.py +++ b/volatility3/framework/plugins/windows/direct_system_calls.py @@ -23,6 +23,7 @@ syscall_finder_type = namedtuple( ], ) + class DirectSystemCalls( interfaces.plugins.PluginInterface, deprecation.PluginRenameClass, @@ -59,4 +60,4 @@ class DirectSystemCalls( ["jmp", "call", "leave", "int3"], # the expected form is to end with a "ret" back to the calling code ["ret"], - ) \ No newline at end of file + ) From 253b274cfbe88fe12b0dc742fa6adfce73f8de84 Mon Sep 17 00:00:00 2001 From: cpuu Date: Wed, 18 Jun 2025 15:35:51 +0900 Subject: [PATCH 157/172] linux-tutorial: update symbol table section - Removed outdated reference to the Linux ISF Server (service no longer available) - Updated symbol table instructions to reflect current volatility3 behavior (symbol files now auto-detected from volatility3/symbols directory) --- doc/source/getting-started-linux-tutorial.rst | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/doc/source/getting-started-linux-tutorial.rst b/doc/source/getting-started-linux-tutorial.rst index 33f82911a..eb4ab7562 100644 --- a/doc/source/getting-started-linux-tutorial.rst +++ b/doc/source/getting-started-linux-tutorial.rst @@ -14,14 +14,11 @@ Volatility3 does not provide the ability to acquire memory. Below are some exam 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 `_. Procedure to create symbol tables for linux --------------------------------------------- +------------------------------------------- -To create a symbol table please refer to :ref:`symbol-tables:Mac or Linux symbol tables`. - -.. tip:: It may be possible to locate pre-made ISF files from the `Linux ISF Server `_ , - which is built and maintained by `kevthehermit `_. - After creating the file or downloading it from the ISF server, place the file under the directory ``volatility3/symbols/linux``. - If necessary create a linux directory under the symbols directory (this will become unnecessary in future versions). +To create a symbol table please refer to :ref:`symbol-tables:Mac or Linux symbol tables`. +After creating the file, place it under the directory ``volatility3/symbols``. +Volatility3 will automatically detect and use symbol tables from this location. Listing plugins From 46609d418a7d25a1632c130f04e6b490a5217d68 Mon Sep 17 00:00:00 2001 From: cpuu Date: Wed, 18 Jun 2025 15:42:35 +0900 Subject: [PATCH 158/172] linux-tutorial: revise plugin listing section - Replaced outdated and partial plugin list with a concise summary - Mentioned total number of supported Linux plugins (~40+) - Highlighted representative plugins such as pslist, bash, lsmod, etc. - Provided updated command to enumerate all available Linux plugins --- doc/source/getting-started-linux-tutorial.rst | 25 +++++++++++-------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/doc/source/getting-started-linux-tutorial.rst b/doc/source/getting-started-linux-tutorial.rst index eb4ab7562..53f44fc4f 100644 --- a/doc/source/getting-started-linux-tutorial.rst +++ b/doc/source/getting-started-linux-tutorial.rst @@ -24,20 +24,25 @@ Volatility3 will automatically detect and use symbol tables from this location. 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 `. -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.malware.check_afinfo.Check_afinfo - linux.malware.check_creds.Check_creds - linux.malware.check_idt.Check_idt + $ python3 vol.py --help | grep -i linux. -.. note:: Here 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 From 25e15f12fad6d27597a8f5f8a6686426c0a68557 Mon Sep 17 00:00:00 2001 From: cpuu Date: Wed, 18 Jun 2025 15:52:08 +0900 Subject: [PATCH 159/172] linux-tutorial: update banners section - Removed outdated instructions referencing the ISF server - Updated guidance to reflect current method of manually generating ISF files - Clarified placement of ISF files under volatility3/symbols for automatic detection --- doc/source/getting-started-linux-tutorial.rst | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/doc/source/getting-started-linux-tutorial.rst b/doc/source/getting-started-linux-tutorial.rst index 53f44fc4f..05571ad07 100644 --- a/doc/source/getting-started-linux-tutorial.rst +++ b/doc/source/getting-started-linux-tutorial.rst @@ -69,7 +69,7 @@ Thanks go to `stuxnet `_ for providing this memo $ 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 @@ -81,10 +81,11 @@ Thanks go to `stuxnet `_ for providing this memo 0x7fde0010 Linux version 4.15.0-72-generic (buildd@lcy01-amd64-026) (gcc version 7.4.0 (Ubuntu 7.4.0-1ubuntu1~18.04.1)) #81-Ubuntu SMP Tue Nov 26 12:20:02 UTC 2019 (Ubuntu 4.15.0-72.81-generic 4.15.18) -The above command helps us to find the memory dump's kernel version and the distribution version. Now using the above banner we can search for the needed ISF file from the ISF server. -If 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. + -.. tip:: Use the banner text which is most repeated to search on the ISF Server. linux.pslist ~~~~~~~~~~~~ From e8f36325ecdcbb7b8a1ee4df833fb5089c3477e5 Mon Sep 17 00:00:00 2001 From: cpuu Date: Wed, 18 Jun 2025 15:59:27 +0900 Subject: [PATCH 160/172] linux-tutorial: add boottime plugin example - Added new section for linux.boottime plugin - Demonstrated how to extract system boot time from memory - Explained its relevance for timeline analysis and incident response --- doc/source/getting-started-linux-tutorial.rst | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/doc/source/getting-started-linux-tutorial.rst b/doc/source/getting-started-linux-tutorial.rst index 05571ad07..147293195 100644 --- a/doc/source/getting-started-linux-tutorial.rst +++ b/doc/source/getting-started-linux-tutorial.rst @@ -85,6 +85,23 @@ The above command helps us identify the kernel version and distribution from the 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. linux.pslist From 5531d76bfc460b4719c9f0a9fa5922875e9f1665 Mon Sep 17 00:00:00 2001 From: cpuu Date: Wed, 18 Jun 2025 16:06:00 +0900 Subject: [PATCH 161/172] linux-tutorial: update pslist and pstree sections - Updated linux.pslist output to include new fields: OFFSET, UID/GID, creation time, and file output - Added detailed explanation of each column and its forensic significance - Revised linux.pstree section to reflect new output format including OFFSET and hierarchical indentation - Emphasized the utility of both plugins for process analysis and anomaly detection --- doc/source/getting-started-linux-tutorial.rst | 83 +++++++------------ 1 file changed, 29 insertions(+), 54 deletions(-) diff --git a/doc/source/getting-started-linux-tutorial.rst b/doc/source/getting-started-linux-tutorial.rst index 147293195..f43c26b66 100644 --- a/doc/source/getting-started-linux-tutorial.rst +++ b/doc/source/getting-started-linux-tutorial.rst @@ -97,6 +97,7 @@ It is useful for establishing a timeline, particularly when analyzing incident r Volatility 3 Framework 2.26.0 Progress: 100.00 Stacking attempts finished + TIME NS Boot Time - 2022-02-10 06:50:16.450008 UTC @@ -107,77 +108,51 @@ This timestamp can serve as a reference point for correlating system events, suc 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. +It is especially useful for identifying unusual or suspicious process structures, such as orphaned child processes, injected children under legitimate parents, or long chains of shell execution. + .. 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 - 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 + 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 + +The tree view can help identify anomalies in process launch sequences or privilege escalations by inspecting unexpected parent-child relationships. -``linux.pstree`` helps us to display the parent-child relationships between processes. linux.bash ~~~~~~~~~~ From 8ea6422420b92d70666dffc80d8cd35116b6160e Mon Sep 17 00:00:00 2001 From: cpuu Date: Wed, 18 Jun 2025 16:15:46 +0900 Subject: [PATCH 162/172] linux-tutorial: add network plugin examples under Using plugins - Added linux.ip.Addr and linux.ip.Link examples to the Using plugins section - Highlighted the importance of network configuration in memory forensics - Explained key fields such as interface state, MAC, IP, namespace, and flags - Structured the content consistently alongside other plugin examples (pslist, bash, etc.) --- doc/source/getting-started-linux-tutorial.rst | 44 ++++++++++++++----- 1 file changed, 32 insertions(+), 12 deletions(-) diff --git a/doc/source/getting-started-linux-tutorial.rst b/doc/source/getting-started-linux-tutorial.rst index f43c26b66..c91340902 100644 --- a/doc/source/getting-started-linux-tutorial.rst +++ b/doc/source/getting-started-linux-tutorial.rst @@ -163,7 +163,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 @@ -172,17 +172,37 @@ 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. + From ce6c43f1f44f105db21fd581c03f3f5835a7b475 Mon Sep 17 00:00:00 2001 From: cpuu Date: Wed, 18 Jun 2025 16:20:57 +0900 Subject: [PATCH 163/172] linux-tutorial: add malfind plugin section - Added new section for linux.malfind plugin under Using plugins - Included example output showing detection of suspicious executable memory regions - Explained how to interpret fields such as anonymous mapping, rwx protection, and disassembly - Highlighted analysis tips for identifying potential code injection or fileless malware --- doc/source/getting-started-linux-tutorial.rst | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/doc/source/getting-started-linux-tutorial.rst b/doc/source/getting-started-linux-tutorial.rst index c91340902..c474a8291 100644 --- a/doc/source/getting-started-linux-tutorial.rst +++ b/doc/source/getting-started-linux-tutorial.rst @@ -206,3 +206,44 @@ Volatility3 provides the following two plugins to examine this information: 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. + + + From 5ce5fe67dc93112d76b18e1fe12118b8d8137fd8 Mon Sep 17 00:00:00 2001 From: cpuu Date: Wed, 18 Jun 2025 16:24:22 +0900 Subject: [PATCH 164/172] linux-tutorial: finalize with plugin discovery and contribution guide - Added concluding section to guide users toward further plugin exploration - Provided command to list all supported Linux plugins in Volatility 3 - Encouraged community contribution by highlighting the open-source nature of the project - Linked to the official Volatility 3 GitHub repository for contributor reference --- doc/source/getting-started-linux-tutorial.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/doc/source/getting-started-linux-tutorial.rst b/doc/source/getting-started-linux-tutorial.rst index c474a8291..c7917b4db 100644 --- a/doc/source/getting-started-linux-tutorial.rst +++ b/doc/source/getting-started-linux-tutorial.rst @@ -245,5 +245,11 @@ In this output: 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. From 17a7fff9268f80cebcd33b6f9dfa669f6cd45458 Mon Sep 17 00:00:00 2001 From: cpuu Date: Wed, 18 Jun 2025 16:30:28 +0900 Subject: [PATCH 165/172] Change link Change link --- doc/source/getting-started-linux-tutorial.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/source/getting-started-linux-tutorial.rst b/doc/source/getting-started-linux-tutorial.rst index c7917b4db..28726d0ec 100644 --- a/doc/source/getting-started-linux-tutorial.rst +++ b/doc/source/getting-started-linux-tutorial.rst @@ -62,7 +62,7 @@ banners ~~~~~~~ In this example we will be using a memory dump from the Insomni'hack teaser 2020 CTF Challenge called Getdents. We will limit the discussion to memory forensics with volatility 3 and not extend it to other parts of the challenge. -Thanks go to `stuxnet `_ for providing this memory dump and `writeup `_. +Thanks go to `stuxnet `_ for providing this memory dump and writeup `_. .. code-block:: shell-session From 9df53004830684dadfb38e505ead8a34eac07b1e Mon Sep 17 00:00:00 2001 From: cpuu Date: Wed, 18 Jun 2025 16:32:14 +0900 Subject: [PATCH 166/172] Edit link link --- doc/source/getting-started-linux-tutorial.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/doc/source/getting-started-linux-tutorial.rst b/doc/source/getting-started-linux-tutorial.rst index 28726d0ec..250a34c88 100644 --- a/doc/source/getting-started-linux-tutorial.rst +++ b/doc/source/getting-started-linux-tutorial.rst @@ -62,7 +62,7 @@ banners ~~~~~~~ In this example we will be using a memory dump from the Insomni'hack teaser 2020 CTF Challenge called Getdents. We will limit the discussion to memory forensics with volatility 3 and not extend it to other parts of the challenge. -Thanks go to `stuxnet `_ for providing this memory dump and writeup `_. +Thanks go to `stuxnet `_ for providing this memory dump and `writeup `_. .. code-block:: shell-session @@ -253,3 +253,4 @@ However, many more plugins are available, covering topics such as kernel modules 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. + From d9a6ff803b583c1dfa49532929ac87cd98cd91c9 Mon Sep 17 00:00:00 2001 From: Jaeyou PARK Date: Mon, 23 Jun 2025 14:53:12 +0900 Subject: [PATCH 167/172] Update getting-started-linux-tutorial.rst Update memory acquisition section: remove deprecated LiME reference LiME has been removed from the documentation due to its unmaintained status. The section now highlights AVML as an actively maintained tool, and includes a general note encouraging users to verify tool compatibility. --- doc/source/getting-started-linux-tutorial.rst | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/doc/source/getting-started-linux-tutorial.rst b/doc/source/getting-started-linux-tutorial.rst index 250a34c88..bb40de208 100644 --- a/doc/source/getting-started-linux-tutorial.rst +++ b/doc/source/getting-started-linux-tutorial.rst @@ -6,12 +6,11 @@ This guide will give you a brief overview of how volatility3 works as well as a Acquiring memory ---------------- -Volatility3 does not provide the ability to acquire memory. Below are some examples of tools that can be used to acquire memory, but more are available: +Volatility3 does not provide the ability to acquire memory. Below is an example of a tool that can be used to acquire memory on Linux systems: * `AVML - Acquire Volatile Memory for Linux `_ -* `LiME - Linux Memory Extract `_ -Be aware that LiME raw format is not supported by volatility3, the padded or lime option should be used instead. `This issue contains further information `_. +Other tools may exist, but please verify their maintenance status and compatibility with volatility3 before use. Procedure to create symbol tables for linux ------------------------------------------- From 0f33734f3bd541118d5c51491bb856c7ed91c880 Mon Sep 17 00:00:00 2001 From: Jaeyou PARK Date: Mon, 23 Jun 2025 15:20:42 +0900 Subject: [PATCH 168/172] Update getting-started-linux-tutorial.rst : Add reference to Abyss-W4tcher/volatility3-symbols Recommend users first check this repository for pre-generated symbol tables by kernel version for popular Linux distributions before creating their own. --- doc/source/getting-started-linux-tutorial.rst | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/doc/source/getting-started-linux-tutorial.rst b/doc/source/getting-started-linux-tutorial.rst index bb40de208..d84872b3e 100644 --- a/doc/source/getting-started-linux-tutorial.rst +++ b/doc/source/getting-started-linux-tutorial.rst @@ -12,14 +12,19 @@ Volatility3 does not provide the ability to acquire memory. Below is an example Other tools may exist, but please verify their maintenance status and compatibility with volatility3 before use. -Procedure to create symbol tables for linux +Procedure to create symbol tables for Linux ------------------------------------------- -To create a symbol table please refer to :ref:`symbol-tables:Mac or Linux symbol tables`. +It is recommended to first check the repository `volatility3-symbols `_ for pre-generated JSON.xz symbol table files. +This repository provides files organized by kernel version for popular Linux distributions such as Debian, Ubuntu, and AlmaLinux. + +If you cannot find a suitable symbol table for your kernel version there, please refer to :ref:`symbol-tables:Mac or Linux symbol tables` to create one manually. + After creating the file, place it under the directory ``volatility3/symbols``. Volatility3 will automatically detect and use symbol tables from this location. + Listing plugins --------------- From 389223795561620ba4ff868976cf81e57ce8722e Mon Sep 17 00:00:00 2001 From: Jaeyou PARK Date: Mon, 23 Jun 2025 15:37:35 +0900 Subject: [PATCH 169/172] Update getting-started-linux-tutorial.rst : Rearrange linux.pstree plugin description Moved plugin output example above the feature explanation for better flow and clarity. Simplified the description while retaining key points about process hierarchy and anomaly detection. --- doc/source/getting-started-linux-tutorial.rst | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/doc/source/getting-started-linux-tutorial.rst b/doc/source/getting-started-linux-tutorial.rst index d84872b3e..d0b097e0e 100644 --- a/doc/source/getting-started-linux-tutorial.rst +++ b/doc/source/getting-started-linux-tutorial.rst @@ -133,9 +133,7 @@ This detailed view allows investigators to correlate user privileges, startup ti linux.pstree ~~~~~~~~~~~~ - -This plugin presents the process hierarchy as a tree, clearly showing parent-child relationships between processes. -It is especially useful for identifying unusual or suspicious process structures, such as orphaned child processes, injected children under legitimate parents, or long chains of shell execution. +This plugin presents the process hierarchy as a tree, clearly showing parent-child relationships between processes. .. code-block:: shell-session @@ -155,7 +153,10 @@ It is especially useful for identifying unusual or suspicious process structures **** 0x8ca671210000 1608 1608 1507 gnome-session-b ***** 0x8ca66fba42c0 1765 1765 1608 ssh-agent -The tree view can help identify anomalies in process launch sequences or privilege escalations by inspecting unexpected parent-child relationships. + +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. + linux.bash From 65b99bc5462f635bf7f4ef5d83f766ed5c385a2e Mon Sep 17 00:00:00 2001 From: SolitudePy <47316655+SolitudePy@users.noreply.github.com> Date: Mon, 23 Jun 2025 12:41:17 +0300 Subject: [PATCH 170/172] Plugins: remove unused unix argument in linux.sockstat --- volatility3/framework/plugins/linux/sockstat.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/volatility3/framework/plugins/linux/sockstat.py b/volatility3/framework/plugins/linux/sockstat.py index a6acf825b..a74e84f92 100644 --- a/volatility3/framework/plugins/linux/sockstat.py +++ b/volatility3/framework/plugins/linux/sockstat.py @@ -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. " From 97006bc61cb7403e7e1fe43389c9724e87247492 Mon Sep 17 00:00:00 2001 From: atcuno Date: Mon, 30 Jun 2025 17:27:58 -0500 Subject: [PATCH 171/172] Change warning to debug to not break plugin output and to conform to coding standards --- volatility3/framework/plugins/windows/malware/malfind.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/volatility3/framework/plugins/windows/malware/malfind.py b/volatility3/framework/plugins/windows/malware/malfind.py index 33eaf64ef..01da93e1f 100644 --- a/volatility3/framework/plugins/windows/malware/malfind.py +++ b/volatility3/framework/plugins/windows/malware/malfind.py @@ -173,7 +173,7 @@ class Malfind(interfaces.plugins.PluginInterface): if dirty_page is not None: # Useful information to investigate the page content with volshell afterwards. - vollog.warning( + vollog.debug( f"[proc_id {proc_id}] Found suspicious DIRTY + {protection_string} page at {hex(dirty_page)}", ) start = vad.get_start() From 1ebb82a0c0ec00c6e54f2d5f731de18520367041 Mon Sep 17 00:00:00 2001 From: Mike Auty Date: Sat, 12 Jul 2025 11:09:47 +0100 Subject: [PATCH 172/172] Try to fix documentation builds --- pyproject.toml | 2 +- volatility3/framework/plugins/yarascan.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index b88ac7752..fca8ea436 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -52,7 +52,7 @@ test = [ 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", ] diff --git a/volatility3/framework/plugins/yarascan.py b/volatility3/framework/plugins/yarascan.py index 040a50c1a..910ded109 100644 --- a/volatility3/framework/plugins/yarascan.py +++ b/volatility3/framework/plugins/yarascan.py @@ -31,7 +31,7 @@ except ImportError: except ImportError: vollog.info( - "Neither yara-x nor yara-python (>3.8.0) module not found, plugin (and dependent plugins) not available" + "Neither yara-x nor yara-python (>3.8.0) module was found, plugin (and dependent plugins) not available" ) raise