From 3fc8b9cd31aaaf9a01b5fe12b4745e4884448c7b Mon Sep 17 00:00:00 2001 From: Mike Auty Date: Mon, 27 Jan 2025 00:04:28 +0000 Subject: [PATCH 01/71] 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 02/71] 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 03/71] 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 04/71] 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 05/71] 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 06/71] 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 07/71] 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 08/71] 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 09/71] 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 10/71] 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 11/71] 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 12/71] 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 13/71] 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 14/71] 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 15/71] 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 16/71] 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 17/71] 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 18/71] 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 19/71] 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 20/71] 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 21/71] 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 22/71] 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 23/71] 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 24/71] 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 25/71] 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 26/71] 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 27/71] 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 28/71] 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 29/71] 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 30/71] 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 31/71] 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 32/71] 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 33/71] 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 34/71] 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 35/71] 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 36/71] 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 37/71] 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 38/71] 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 39/71] 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 40/71] 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 41/71] 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 42/71] 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 43/71] 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 44/71] 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 45/71] 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 46/71] 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 47/71] 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 48/71] 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 49/71] 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 50/71] 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 51/71] 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 52/71] 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 53/71] 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 54/71] 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 55/71] 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 56/71] 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 57/71] 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 58/71] 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 d3da34ce10b6c0dbf56ad54b50498a43a4fcfeef Mon Sep 17 00:00:00 2001 From: Mike Auty Date: Sun, 27 Apr 2025 16:18:47 +0100 Subject: [PATCH 59/71] 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 60/71] 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 61/71] 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 62/71] 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 63/71] 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 64/71] 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 65/71] 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 66/71] 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 67/71] 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 68/71] 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 69/71] 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 70/71] 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 71/71] 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 (