From 3fc8b9cd31aaaf9a01b5fe12b4745e4884448c7b Mon Sep 17 00:00:00 2001 From: Mike Auty Date: Mon, 27 Jan 2025 00:04:28 +0000 Subject: [PATCH 01/60] 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/60] 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/60] 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/60] 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/60] 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/60] 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/60] 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/60] 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 5cc8e360731b27563290f4e40da73b0d00e02364 Mon Sep 17 00:00:00 2001 From: Mike Auty Date: Tue, 8 Apr 2025 16:04:25 +0100 Subject: [PATCH 09/60] 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 ac8a1101ce317a8724d8c68f45b3b090493ce981 Mon Sep 17 00:00:00 2001 From: ikelos Date: Tue, 8 Apr 2025 20:18:10 +0100 Subject: [PATCH 10/60] 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 c48ad901b03c72e7c1ceffe3712dc8322c19de38 Mon Sep 17 00:00:00 2001 From: David McDonald Date: Thu, 10 Apr 2025 14:49:50 -0500 Subject: [PATCH 11/60] 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 12/60] 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 13/60] 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 14/60] 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 15/60] 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 16/60] 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 17/60] 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 18/60] 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 19/60] 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 20/60] 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 21/60] 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 22/60] 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 23/60] 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 24/60] 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 25/60] 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 26/60] 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 27/60] 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 28/60] 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 29/60] 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 30/60] 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 31/60] 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 32/60] 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 33/60] 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 34/60] 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 35/60] 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 36/60] 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 37/60] 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 38/60] 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 39/60] 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 40/60] 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 41/60] 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 42/60] 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 43/60] 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 44/60] 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 45/60] 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 46/60] 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 47/60] 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 48/60] 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 49/60] 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 50/60] 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 51/60] 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 52/60] 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 53/60] 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 54/60] 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 55/60] 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 56/60] 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 57/60] 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 58/60] Fix lint errors --- volatility3/framework/plugins/windows/etwpatch.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/volatility3/framework/plugins/windows/etwpatch.py b/volatility3/framework/plugins/windows/etwpatch.py index a21cab030..14fbacc28 100644 --- a/volatility3/framework/plugins/windows/etwpatch.py +++ b/volatility3/framework/plugins/windows/etwpatch.py @@ -86,17 +86,16 @@ class EtwPatch(interfaces.plugins.PluginInterface): # Map of opcodes to their instruction names opcode_map = { - 0xc3: "RET", - 0xe9: "JMP", + 0xC3: "RET", + 0xE9: "JMP", } for dll_name, functions in found_symbols.items(): for func_name, func_addr in functions: try: - opcode = ( - self.context.layers[proc_layer_name] - .read(func_addr, 1)[0] - ) + opcode = self.context.layers[proc_layer_name].read( + func_addr, 1 + )[0] if opcode in opcode_map: instruction = opcode_map[opcode] yield ( From 6940af98ae2e736d4f6256f130e7a9b00269ce19 Mon Sep 17 00:00:00 2001 From: Andrew Case Date: Thu, 24 Oct 2024 15:11:52 -0500 Subject: [PATCH 59/60] Add useful implementation of yara scanning to Linux --- .../framework/plugins/linux/vmayarascan.py | 107 ++++++++++-------- 1 file changed, 57 insertions(+), 50 deletions(-) diff --git a/volatility3/framework/plugins/linux/vmayarascan.py b/volatility3/framework/plugins/linux/vmayarascan.py index 64f5827c1..cda502a73 100644 --- a/volatility3/framework/plugins/linux/vmayarascan.py +++ b/volatility3/framework/plugins/linux/vmayarascan.py @@ -2,7 +2,6 @@ # which is available at https://www.volatilityfoundation.org/license/vsl-v1.0 # -import logging from typing import Iterable, List, Tuple from volatility3.framework import interfaces, renderers @@ -11,14 +10,12 @@ from volatility3.framework.renderers import format_hints from volatility3.plugins import yarascan from volatility3.plugins.linux import pslist -vollog = logging.getLogger(__name__) - class VmaYaraScan(interfaces.plugins.PluginInterface): """Scans all virtual memory areas for tasks using yara.""" _required_framework_version = (2, 22, 0) - _version = (1, 0, 4) + _version = (1, 0, 0) @classmethod def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]: @@ -30,14 +27,14 @@ class VmaYaraScan(interfaces.plugins.PluginInterface): description="Process IDs to include (all other processes are excluded)", optional=True, ), - requirements.VersionRequirement( - name="pslist", component=pslist.PsList, version=(4, 0, 0) + requirements.PluginRequirement( + name="pslist", plugin=pslist.PsList, version=(4, 0, 1) ), - requirements.VersionRequirement( - name="yarascan", component=yarascan.YaraScan, version=(2, 0, 0) + requirements.PluginRequirement( + name="yarascan", plugin=yarascan.YaraScan, version=(2, 0, 1) ), - requirements.VersionRequirement( - name="yarascanner", component=yarascan.YaraScanner, version=(2, 0, 0) + requirements.PluginRequirement( + name="yarascanner", plugin=yarascan.YaraScanner, version=(2, 1, 1) ), requirements.ModuleRequirement( name="kernel", @@ -56,8 +53,6 @@ class VmaYaraScan(interfaces.plugins.PluginInterface): # use yarascan to parse the yara options provided and create the rules rules = yarascan.YaraScan.process_yara_options(dict(self.config)) - sanity_check = 1024 * 1024 * 1024 # 1 GB - # filter based on the pid option if provided filter_func = pslist.PsList.create_pid_filter(self.config.get("pid", None)) for task in pslist.PsList.list_tasks( @@ -74,46 +69,58 @@ class VmaYaraScan(interfaces.plugins.PluginInterface): # get the proc_layer object from the context proc_layer = self.context.layers[proc_layer_name] - max_vma_size = 0 - vma_maps_to_scan = [] - for start, size in self.get_vma_maps(task): - if size > sanity_check: - vollog.debug( - f"VMA at 0x{start:x} over sanity-check size, not scanning" - ) - continue - max_vma_size = max(max_vma_size, size) - vma_maps_to_scan.append((start, size)) + for start, end in self.get_vma_maps(task): + data = proc_layer.read(start, end - start, True) + if not yarascan.YaraScan._yara_x: + for match in rules.match(data=data): + if yarascan.YaraScan.yara_returns_instances(): + for match_string in match.strings: + for instance in match_string.instances: + yield 0, ( + format_hints.Hex(instance.offset + start), + task.tgid, + match.rule, + match_string.identifier, + renderers.LayerData( + self.context, + proc_layer_name, + instance.offset + start, + instance.matched_length, + ), + ) + else: + for offset, name, value in match.strings: + yield 0, ( + format_hints.Hex(offset + start), + task.tgid, + match.rule, + name, + renderers.LayerData( + self.context, + proc_layer_name, + offset + start, + len(value), + ), + ) + else: + for match in rules.scan(data).matching_rules: + for match_string in match.patterns: + for instance in match_string.matches: + yield 0, ( + format_hints.Hex(instance.offset + start), + task.tgid, + f"{match.namespace}.{match.identifier}", + match_string.identifier, + renderers.LayerData( + self.context, + proc_layer_name, + instance.offset + start, + instance.length, + ), + ) - if not vma_maps_to_scan: - vollog.warning(f"No VMAs were found for task {task.tgid}, not scanning") - continue - - scanner = yarascan.YaraScanner(rules=rules) - scanner.chunk_size = max_vma_size - - # scan the VMA data (in one contiguous block) with the yarascanner - for start, size in vma_maps_to_scan: - for offset, rule_name, name, value in scanner( - proc_layer.read(start, size, pad=True), start - ): - layer_data = renderers.LayerData( - context=self.context, - offset=offset, - layer_name=proc_layer.name, - length=len(value), - ) - yield 0, ( - format_hints.Hex(offset), - task.tgid, - rule_name, - name, - layer_data, - ) - - @classmethod + @staticmethod def get_vma_maps( - cls, task: interfaces.objects.ObjectInterface, ) -> Iterable[Tuple[int, int]]: """Creates a map of start/end addresses for each virtual memory area in a task. From 01e234d78c6dfadf5f7d14420a759c6ca332d20e Mon Sep 17 00:00:00 2001 From: David McDonald Date: Mon, 5 May 2025 09:41:07 -0500 Subject: [PATCH 60/60] YaraScan: Add context bytes option Makes yarascan results' `LayerData` have a configurable context window size. --- .../framework/plugins/linux/vmayarascan.py | 24 ++++++++++++++----- .../framework/plugins/windows/vadyarascan.py | 6 +++-- volatility3/framework/plugins/yarascan.py | 16 +++++++++++-- 3 files changed, 36 insertions(+), 10 deletions(-) diff --git a/volatility3/framework/plugins/linux/vmayarascan.py b/volatility3/framework/plugins/linux/vmayarascan.py index cda502a73..e1e3c1e2a 100644 --- a/volatility3/framework/plugins/linux/vmayarascan.py +++ b/volatility3/framework/plugins/linux/vmayarascan.py @@ -84,8 +84,12 @@ class VmaYaraScan(interfaces.plugins.PluginInterface): renderers.LayerData( self.context, proc_layer_name, - instance.offset + start, - instance.matched_length, + instance.offset + + start + - abs(self.config["context_before"]), + instance.matched_length + + abs(self.config["context_before"]) + + abs(self.config["context_after"]), ), ) else: @@ -98,8 +102,12 @@ class VmaYaraScan(interfaces.plugins.PluginInterface): renderers.LayerData( self.context, proc_layer_name, - offset + start, - len(value), + offset + + start + - abs(self.config["context_before"]), + len(value) + + abs(self.config["context_before"]) + + abs(self.config["context_after"]), ), ) else: @@ -114,8 +122,12 @@ class VmaYaraScan(interfaces.plugins.PluginInterface): renderers.LayerData( self.context, proc_layer_name, - instance.offset + start, - instance.length, + instance.offset + + start + - abs(self.config["context_before"]), + instance.length + + abs(self.config["context_before"]) + + abs(self.config["context_after"]), ), ) diff --git a/volatility3/framework/plugins/windows/vadyarascan.py b/volatility3/framework/plugins/windows/vadyarascan.py index 04b9aadd9..ef40be0cb 100644 --- a/volatility3/framework/plugins/windows/vadyarascan.py +++ b/volatility3/framework/plugins/windows/vadyarascan.py @@ -95,9 +95,11 @@ class VadYaraScan(interfaces.plugins.PluginInterface): ): layer_data = renderers.LayerData( context=self.context, - offset=offset, + offset=offset - abs(self.config["context_before"]), layer_name=layer.name, - length=len(value), + length=len(value) + + abs(self.config["context_before"]) + + abs(self.config["context_after"]), ) yield 0, ( format_hints.Hex(offset), diff --git a/volatility3/framework/plugins/yarascan.py b/volatility3/framework/plugins/yarascan.py index 49db2af02..5a5e6a0c7 100644 --- a/volatility3/framework/plugins/yarascan.py +++ b/volatility3/framework/plugins/yarascan.py @@ -169,6 +169,16 @@ class YaraScan(plugins.PluginInterface): description="Set the maximum size (default is 1GB)", optional=True, ), + requirements.IntRequirement( + name="context_before", + optional=True, + default=0, + ), + requirements.IntRequirement( + name="context_after", + optional=True, + default=0, + ), ] @classmethod @@ -208,9 +218,11 @@ class YaraScan(plugins.PluginInterface): ): layer_data = renderers.LayerData( context=self.context, - offset=offset, + offset=offset - abs(self.config["context_before"]), layer_name=layer.name, - length=len(value), + length=len(value) + + abs(self.config["context_before"]) + + abs(self.config["context_after"]), ) yield 0, (format_hints.Hex(offset), rule_name, name, layer_data)