From 55151f546d0e1ccc65c034075eaaaba324cf4734 Mon Sep 17 00:00:00 2001 From: Mike Auty Date: Fri, 22 Nov 2024 11:46:04 +0000 Subject: [PATCH 01/68] Initial work on adding a LayerData renderer type --- volatility3/framework/interfaces/renderers.py | 32 ++++++++++++++++--- 1 file changed, 28 insertions(+), 4 deletions(-) diff --git a/volatility3/framework/interfaces/renderers.py b/volatility3/framework/interfaces/renderers.py index e26164ee7..477d743d1 100644 --- a/volatility3/framework/interfaces/renderers.py +++ b/volatility3/framework/interfaces/renderers.py @@ -9,8 +9,10 @@ renderer interface which can interact with a TreeGrid to produce suitable output. """ +from dataclasses import dataclass import datetime -from abc import abstractmethod, ABCMeta +from volatility3.framework import interfaces +from abc import ABCMeta, abstractmethod from collections import abc from typing import ( Any, @@ -20,9 +22,9 @@ from typing import ( List, NamedTuple, Optional, - TypeVar, - Type, Tuple, + Type, + TypeVar, Union, ) @@ -124,6 +126,13 @@ class Disassembly: self.offset = offset +@dataclass +class LayerData(object): + layer_name: str + offset: int + length: int + + # We don't class these off a shared base, because the BaseTypes must only # contain the types that the validator will accept (which would not include the base) @@ -136,6 +145,7 @@ BaseTypes = Union[ Type[datetime.datetime], Type[BaseAbsentValue], Type[Disassembly], + Type[LayerData], ] ColumnsType = List[Tuple[str, BaseTypes]] VisitorSignature = Callable[[TreeNode, _Type], _Type] @@ -163,7 +173,12 @@ class TreeGrid(metaclass=ABCMeta): Disassembly, ) - def __init__(self, columns: ColumnsType, generator: Generator) -> None: + def __init__( + self, + columns: ColumnsType, + generator: Generator, + context: Optional[interfaces.context.ContextInterface] = None, + ) -> None: """Constructs a TreeGrid object using a specific set of columns. The TreeGrid itself is a root element, that can have children but no values. @@ -174,6 +189,15 @@ class TreeGrid(metaclass=ABCMeta): columns: A list of column tuples made up of (name, type). generator: An iterable containing row for a tree grid, each row contains a indent level followed by the values for each column in order. """ + self._context = context + + @property + def context(self) -> Optional[interfaces.context.ContextInterface]: + """Returns the context value for the tree grid (to retrieve data items) + + This is a property to ensure the renderers don't try changing the context for any reason + """ + return self._context @staticmethod @abstractmethod From 453f52ba3e192643b8729a45e0d0bb6b4dd5d7d0 Mon Sep 17 00:00:00 2001 From: Mike Auty Date: Tue, 31 Dec 2024 23:04:58 +0000 Subject: [PATCH 02/68] CLI: Add in concept of CellRenderer --- volatility3/cli/text_renderer.py | 74 +++++++++++-------- volatility3/framework/interfaces/renderers.py | 58 ++++++++++++--- 2 files changed, 92 insertions(+), 40 deletions(-) diff --git a/volatility3/cli/text_renderer.py b/volatility3/cli/text_renderer.py index b1944ae5a..6cfd10ced 100644 --- a/volatility3/cli/text_renderer.py +++ b/volatility3/cli/text_renderer.py @@ -9,10 +9,11 @@ import random import string import sys from functools import wraps -from typing import Any, Callable, Dict, List, Tuple +from typing import Any, Callable, Dict, List, Tuple, TypeVar from volatility3.cli import text_filter from volatility3.framework import exceptions, interfaces, renderers +from volatility3.framework.interfaces.renderers import BaseAbsentValue from volatility3.framework.renderers import format_hints vollog = logging.getLogger(__name__) @@ -79,8 +80,9 @@ def multitypedata_as_text(value: format_hints.MultiTypeData) -> str: return string_representation.split("\x00")[0] return hex_bytes_as_text(value) +T = TypeVar("T") -def optional(func: Callable) -> Callable: +def optional(func: Callable[[BaseAbsentValue| T], str]) -> Callable[[T], str]: @wraps(func) def wrapped(x: Any) -> str: if isinstance(x, interfaces.renderers.BaseAbsentValue): @@ -137,9 +139,41 @@ def display_disassembly(disasm: interfaces.renderers.Disassembly) -> str: return QuickTextRenderer._type_renderers[bytes](disasm.data) +class CLITypeRenderer(interfaces.renderers.TypeRendererInterface): + def __init__(self, func): + super().__init__(func = optional(func)) + + +class LayerDataRenderer(CLITypeRenderer): + """Renders a LayerData object into data/bytes""" + def __init__(self): + def render(data: interfaces.renderers.LayerData| BaseAbsentValue): + if isinstance(data, BaseAbsentValue): + # FIXME: Do something cleverer here + return "" + data = data.context.layers[data.layer_name].read(data.offset, data.length) + return " ".join(f"{b:02x}" for b in data) + + render_func = render + return super().__init__(render_func) + + class CLIRenderer(interfaces.renderers.Renderer): """Class to add specific requirements for CLI renderers.""" + _type_renderers = { + format_hints.Bin: CLITypeRenderer(lambda x: f"0b{x:b}"), + format_hints.Hex: CLITypeRenderer(lambda x: f"0x{x:x}"), + format_hints.HexBytes: CLITypeRenderer(hex_bytes_as_text), + format_hints.MultiTypeData: CLITypeRenderer(multitypedata_as_text), + interfaces.renderers.Disassembly: CLITypeRenderer(display_disassembly), + bytes: CLITypeRenderer(lambda x: " ".join(f"{b:02x}" for b in x)), + interfaces.renderers.LayerData: LayerDataRenderer(), + datetime.datetime: CLITypeRenderer(lambda x: x.strftime("%Y-%m-%d %H:%M:%S.%f %Z")), + "default": CLITypeRenderer(lambda x: f"{x}"), + } + + name = "unnamed" structured_output = False filter: text_filter.CLIFilter = None @@ -170,21 +204,11 @@ class CLIRenderer(interfaces.renderers.Renderer): class QuickTextRenderer(CLIRenderer): - _type_renderers = { - format_hints.Bin: optional(lambda x: f"0b{x:b}"), - format_hints.Hex: optional(lambda x: f"0x{x:x}"), - format_hints.HexBytes: optional(hex_bytes_as_text), - format_hints.MultiTypeData: quoted_optional(multitypedata_as_text), - interfaces.renderers.Disassembly: optional(display_disassembly), - bytes: optional(lambda x: " ".join(f"{b:02x}" for b in x)), - datetime.datetime: optional(lambda x: x.strftime("%Y-%m-%d %H:%M:%S.%f %Z")), - "default": optional(lambda x: f"{x}"), - } name = "quick" def get_render_options(self): - pass + return [] def render(self, grid: interfaces.renderers.TreeGrid) -> None: """Renders each column immediately to stdout. @@ -242,7 +266,7 @@ class NoneRenderer(CLIRenderer): name = "none" def get_render_options(self): - pass + return [] def render(self, grid: interfaces.renderers.TreeGrid) -> None: if not grid.populated: @@ -250,22 +274,12 @@ class NoneRenderer(CLIRenderer): class CSVRenderer(CLIRenderer): - _type_renderers = { - format_hints.Bin: optional(lambda x: f"0b{x:b}"), - format_hints.Hex: optional(lambda x: f"0x{x:x}"), - format_hints.HexBytes: optional(hex_bytes_as_text), - format_hints.MultiTypeData: optional(multitypedata_as_text), - interfaces.renderers.Disassembly: optional(display_disassembly), - bytes: optional(lambda x: " ".join(f"{b:02x}" for b in x)), - datetime.datetime: optional(lambda x: x.strftime("%Y-%m-%d %H:%M:%S.%f %Z")), - "default": optional(lambda x: f"{x}"), - } name = "csv" structured_output = True def get_render_options(self): - pass + return [] def render(self, grid: interfaces.renderers.TreeGrid) -> None: """Renders each row immediately to stdout. @@ -316,12 +330,10 @@ class CSVRenderer(CLIRenderer): class PrettyTextRenderer(CLIRenderer): - _type_renderers = QuickTextRenderer._type_renderers - name = "pretty" def get_render_options(self): - pass + return [] def render(self, grid: interfaces.renderers.TreeGrid) -> None: """Renders each column immediately to stdout. @@ -380,7 +392,7 @@ class PrettyTextRenderer(CLIRenderer): accumulator.append((node.path_depth, line)) return accumulator - final_output: List[Tuple[int, Dict[interfaces.renderers.Column, bytes]]] = [] + final_output: List[Tuple[int, Dict[interfaces.renderers.Column, str]]] = [] if not grid.populated: grid.populate(visitor, final_output) else: @@ -418,7 +430,7 @@ class PrettyTextRenderer(CLIRenderer): del line[column] else: line[column] = line[column] + ( - [""] * (nums_line - len(line[column])) + "" * (nums_line - len(line[column])) ) for index in range(nums_line): if index == 0: @@ -463,7 +475,7 @@ class JsonRenderer(CLIRenderer): structured_output = True def get_render_options(self) -> List[interfaces.renderers.RenderOption]: - pass + return [] def output_result(self, outfd, result): """Outputs the JSON data to a file in a particular format""" diff --git a/volatility3/framework/interfaces/renderers.py b/volatility3/framework/interfaces/renderers.py index 477d743d1..6aa0ad7aa 100644 --- a/volatility3/framework/interfaces/renderers.py +++ b/volatility3/framework/interfaces/renderers.py @@ -9,9 +9,8 @@ renderer interface which can interact with a TreeGrid to produce suitable output. """ -from dataclasses import dataclass +import dataclasses import datetime -from volatility3.framework import interfaces from abc import ABCMeta, abstractmethod from collections import abc from typing import ( @@ -27,6 +26,14 @@ from typing import ( TypeVar, Union, ) +from typing import Dict +import functools + +from volatility3.framework import interfaces + + +class BaseAbsentValue: + """Class that represents values which are not present for some reason.""" class Column(NamedTuple): @@ -36,11 +43,34 @@ class Column(NamedTuple): RenderOption = Any +T = TypeVar("T") + +class TypeRendererInterface: + type = T + + def __init__(self, func: Optional[Callable] = None, options: Optional[Dict[str, Any]] = None): + self._options = options or {} + setattr(self, "render", func) + + @property + def options(self): + return self._options + + def render(self, data: T|BaseAbsentValue) -> Any: + """Renders a specific datatype""" + return "" + + def __call__(self, data: T|BaseAbsentValue) -> Any: + """Shortcut for render""" + return self.render(data) + class Renderer(metaclass=ABCMeta): """Class that defines the interface that all output renderers must support.""" + _type_renderers: Dict[Union[Type, str], Callable] + def __init__(self, options: Optional[List[RenderOption]] = None) -> None: """Accepts an options object to configure the renderers.""" # FIXME: Once the config option objects are in place, put the _type_check in place @@ -104,10 +134,6 @@ class TreeNode(abc.Sequence, metaclass=ABCMeta): """ -class BaseAbsentValue: - """Class that represents values which are not present for some reason.""" - - class Disassembly: """A class to indicate that the bytes provided should be disassembled (based on the architecture)""" @@ -126,12 +152,24 @@ class Disassembly: self.offset = offset -@dataclass +@dataclasses.dataclass class LayerData(object): + """Layer data + + This requires the contex to be passed in, in case plugins want to use multiple contexts + and to ensure the TreeGrid interface doesn't change, since this would break all existing plugins""" + context: 'interfaces.context.ContextInterface' layer_name: str offset: int length: int + @staticmethod + def from_object(object: 'interfaces.objects.ObjectInterface', size: Optional[int] = None): + return LayerData(context = object._context, + layer_name = object.vol.layer_name, + offset = object.vol.offset, + length = size or object.vol.size) + # We don't class these off a shared base, because the BaseTypes must only # contain the types that the validator will accept (which would not include the base) @@ -164,6 +202,7 @@ class TreeGrid(metaclass=ABCMeta): and to create cycles. """ + # TODO: Figure out why this isn't just BaseTypes (which includes AbsentValues' base_types: ClassVar[Tuple] = ( int, str, @@ -171,13 +210,14 @@ class TreeGrid(metaclass=ABCMeta): bytes, datetime.datetime, Disassembly, + LayerData ) def __init__( self, columns: ColumnsType, generator: Generator, - context: Optional[interfaces.context.ContextInterface] = None, + context: Optional['interfaces.context.ContextInterface'] = None, ) -> None: """Constructs a TreeGrid object using a specific set of columns. @@ -192,7 +232,7 @@ class TreeGrid(metaclass=ABCMeta): self._context = context @property - def context(self) -> Optional[interfaces.context.ContextInterface]: + def context(self) -> Optional['interfaces.context.ContextInterface']: """Returns the context value for the tree grid (to retrieve data items) This is a property to ensure the renderers don't try changing the context for any reason From 1ecd75f6653981eae52de51ee322423a1bc5c9e0 Mon Sep 17 00:00:00 2001 From: Mike Auty Date: Sun, 9 Feb 2025 22:08:11 +0000 Subject: [PATCH 03/68] Core: Apply black to interfaces and the CLI --- volatility3/cli/text_renderer.py | 18 +++++----- volatility3/framework/interfaces/renderers.py | 35 ++++++++++++------- 2 files changed, 32 insertions(+), 21 deletions(-) diff --git a/volatility3/cli/text_renderer.py b/volatility3/cli/text_renderer.py index 6cfd10ced..07a8e3867 100644 --- a/volatility3/cli/text_renderer.py +++ b/volatility3/cli/text_renderer.py @@ -80,9 +80,11 @@ def multitypedata_as_text(value: format_hints.MultiTypeData) -> str: return string_representation.split("\x00")[0] return hex_bytes_as_text(value) + T = TypeVar("T") -def optional(func: Callable[[BaseAbsentValue| T], str]) -> Callable[[T], str]: + +def optional(func: Callable[[BaseAbsentValue | T], str]) -> Callable[[T], str]: @wraps(func) def wrapped(x: Any) -> str: if isinstance(x, interfaces.renderers.BaseAbsentValue): @@ -141,13 +143,14 @@ def display_disassembly(disasm: interfaces.renderers.Disassembly) -> str: class CLITypeRenderer(interfaces.renderers.TypeRendererInterface): def __init__(self, func): - super().__init__(func = optional(func)) + super().__init__(func=optional(func)) class LayerDataRenderer(CLITypeRenderer): """Renders a LayerData object into data/bytes""" + def __init__(self): - def render(data: interfaces.renderers.LayerData| BaseAbsentValue): + def render(data: interfaces.renderers.LayerData | BaseAbsentValue): if isinstance(data, BaseAbsentValue): # FIXME: Do something cleverer here return "" @@ -169,11 +172,12 @@ class CLIRenderer(interfaces.renderers.Renderer): interfaces.renderers.Disassembly: CLITypeRenderer(display_disassembly), bytes: CLITypeRenderer(lambda x: " ".join(f"{b:02x}" for b in x)), interfaces.renderers.LayerData: LayerDataRenderer(), - datetime.datetime: CLITypeRenderer(lambda x: x.strftime("%Y-%m-%d %H:%M:%S.%f %Z")), + datetime.datetime: CLITypeRenderer( + lambda x: x.strftime("%Y-%m-%d %H:%M:%S.%f %Z") + ), "default": CLITypeRenderer(lambda x: f"{x}"), } - name = "unnamed" structured_output = False filter: text_filter.CLIFilter = None @@ -429,9 +433,7 @@ class PrettyTextRenderer(CLIRenderer): if column in ignore_columns: del line[column] else: - line[column] = line[column] + ( - "" * (nums_line - len(line[column])) - ) + line[column] = line[column] + ("" * (nums_line - len(line[column]))) for index in range(nums_line): if index == 0: outfd.write( diff --git a/volatility3/framework/interfaces/renderers.py b/volatility3/framework/interfaces/renderers.py index 6aa0ad7aa..ec552cf62 100644 --- a/volatility3/framework/interfaces/renderers.py +++ b/volatility3/framework/interfaces/renderers.py @@ -45,10 +45,13 @@ RenderOption = Any T = TypeVar("T") + class TypeRendererInterface: type = T - def __init__(self, func: Optional[Callable] = None, options: Optional[Dict[str, Any]] = None): + def __init__( + self, func: Optional[Callable] = None, options: Optional[Dict[str, Any]] = None + ): self._options = options or {} setattr(self, "render", func) @@ -56,11 +59,11 @@ class TypeRendererInterface: def options(self): return self._options - def render(self, data: T|BaseAbsentValue) -> Any: + def render(self, data: T | BaseAbsentValue) -> Any: """Renders a specific datatype""" return "" - def __call__(self, data: T|BaseAbsentValue) -> Any: + def __call__(self, data: T | BaseAbsentValue) -> Any: """Shortcut for render""" return self.render(data) @@ -157,18 +160,24 @@ class LayerData(object): """Layer data This requires the contex to be passed in, in case plugins want to use multiple contexts - and to ensure the TreeGrid interface doesn't change, since this would break all existing plugins""" - context: 'interfaces.context.ContextInterface' + and to ensure the TreeGrid interface doesn't change, since this would break all existing plugins + """ + + context: "interfaces.context.ContextInterface" layer_name: str offset: int length: int @staticmethod - def from_object(object: 'interfaces.objects.ObjectInterface', size: Optional[int] = None): - return LayerData(context = object._context, - layer_name = object.vol.layer_name, - offset = object.vol.offset, - length = size or object.vol.size) + def from_object( + object: "interfaces.objects.ObjectInterface", size: Optional[int] = None + ): + return LayerData( + context=object._context, + layer_name=object.vol.layer_name, + offset=object.vol.offset, + length=size or object.vol.size, + ) # We don't class these off a shared base, because the BaseTypes must only @@ -210,14 +219,14 @@ class TreeGrid(metaclass=ABCMeta): bytes, datetime.datetime, Disassembly, - LayerData + LayerData, ) def __init__( self, columns: ColumnsType, generator: Generator, - context: Optional['interfaces.context.ContextInterface'] = None, + context: Optional["interfaces.context.ContextInterface"] = None, ) -> None: """Constructs a TreeGrid object using a specific set of columns. @@ -232,7 +241,7 @@ class TreeGrid(metaclass=ABCMeta): self._context = context @property - def context(self) -> Optional['interfaces.context.ContextInterface']: + def context(self) -> Optional["interfaces.context.ContextInterface"]: """Returns the context value for the tree grid (to retrieve data items) This is a property to ensure the renderers don't try changing the context for any reason From 4fd501d38fe1cadae269edcb1edd503290923103 Mon Sep 17 00:00:00 2001 From: Mike Auty Date: Sun, 9 Feb 2025 22:12:14 +0000 Subject: [PATCH 04/68] Core: Resolve ruff errors --- volatility3/framework/interfaces/renderers.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/volatility3/framework/interfaces/renderers.py b/volatility3/framework/interfaces/renderers.py index ec552cf62..1fe61f88f 100644 --- a/volatility3/framework/interfaces/renderers.py +++ b/volatility3/framework/interfaces/renderers.py @@ -27,7 +27,6 @@ from typing import ( Union, ) from typing import Dict -import functools from volatility3.framework import interfaces @@ -156,7 +155,7 @@ class Disassembly: @dataclasses.dataclass -class LayerData(object): +class LayerData: """Layer data This requires the contex to be passed in, in case plugins want to use multiple contexts From 16d1b2697c7e2bc220028337d3e8b3350773f5b9 Mon Sep 17 00:00:00 2001 From: Mike Auty Date: Sun, 9 Feb 2025 22:16:40 +0000 Subject: [PATCH 05/68] Core: Fix typing for python < 3.10 --- volatility3/framework/interfaces/renderers.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/volatility3/framework/interfaces/renderers.py b/volatility3/framework/interfaces/renderers.py index 1fe61f88f..f6cac1859 100644 --- a/volatility3/framework/interfaces/renderers.py +++ b/volatility3/framework/interfaces/renderers.py @@ -58,11 +58,11 @@ class TypeRendererInterface: def options(self): return self._options - def render(self, data: T | BaseAbsentValue) -> Any: + def render(self, data: Union[T,BaseAbsentValue] -> Any: """Renders a specific datatype""" return "" - def __call__(self, data: T | BaseAbsentValue) -> Any: + def __call__(self, data: Union[T, BaseAbsentValue]) -> Any: """Shortcut for render""" return self.render(data) From 45ee399623814b7737962bebc7ea89f1546fa1dd Mon Sep 17 00:00:00 2001 From: Mike Auty Date: Sun, 9 Feb 2025 22:18:25 +0000 Subject: [PATCH 06/68] Core: Fix up yet another typo --- volatility3/framework/interfaces/renderers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/volatility3/framework/interfaces/renderers.py b/volatility3/framework/interfaces/renderers.py index f6cac1859..e3fc02573 100644 --- a/volatility3/framework/interfaces/renderers.py +++ b/volatility3/framework/interfaces/renderers.py @@ -58,7 +58,7 @@ class TypeRendererInterface: def options(self): return self._options - def render(self, data: Union[T,BaseAbsentValue] -> Any: + def render(self, data: Union[T,BaseAbsentValue]) -> Any: """Renders a specific datatype""" return "" From d3a5883130f78fa7467ac24cfde49fbfc27acff4 Mon Sep 17 00:00:00 2001 From: Mike Auty Date: Sun, 9 Feb 2025 22:24:33 +0000 Subject: [PATCH 07/68] Core: Fix up more bad typing operators --- volatility3/cli/text_renderer.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/volatility3/cli/text_renderer.py b/volatility3/cli/text_renderer.py index 07a8e3867..a13c64d0d 100644 --- a/volatility3/cli/text_renderer.py +++ b/volatility3/cli/text_renderer.py @@ -9,7 +9,7 @@ import random import string import sys from functools import wraps -from typing import Any, Callable, Dict, List, Tuple, TypeVar +from typing import Any, Callable, Dict, List, Tuple, TypeVar, Union from volatility3.cli import text_filter from volatility3.framework import exceptions, interfaces, renderers @@ -84,7 +84,7 @@ def multitypedata_as_text(value: format_hints.MultiTypeData) -> str: T = TypeVar("T") -def optional(func: Callable[[BaseAbsentValue | T], str]) -> Callable[[T], str]: +def optional(func: Callable[[Union[BaseAbsentValue, T]], str]) -> Callable[[T], str]: @wraps(func) def wrapped(x: Any) -> str: if isinstance(x, interfaces.renderers.BaseAbsentValue): @@ -150,7 +150,7 @@ class LayerDataRenderer(CLITypeRenderer): """Renders a LayerData object into data/bytes""" def __init__(self): - def render(data: interfaces.renderers.LayerData | BaseAbsentValue): + def render(data: Union[interfaces.renderers.LayerData, BaseAbsentValue]): if isinstance(data, BaseAbsentValue): # FIXME: Do something cleverer here return "" From ba82067dac96ebf3845e37addf3d0d71f0a84462 Mon Sep 17 00:00:00 2001 From: Mike Auty Date: Sat, 15 Feb 2025 20:29:59 +0000 Subject: [PATCH 08/68] CLI: Add in initial LayerData renderer --- volatility3/cli/text_renderer.py | 61 ++++++++++++++++++++++++++++++-- 1 file changed, 59 insertions(+), 2 deletions(-) diff --git a/volatility3/cli/text_renderer.py b/volatility3/cli/text_renderer.py index a13c64d0d..a0b45e353 100644 --- a/volatility3/cli/text_renderer.py +++ b/volatility3/cli/text_renderer.py @@ -1,3 +1,5 @@ +from volatility3.framework.interfaces.layers import TranslationLayerInterface + # 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 # @@ -150,12 +152,67 @@ class LayerDataRenderer(CLITypeRenderer): """Renders a LayerData object into data/bytes""" def __init__(self): + self.context_byte_len = 0 + self.width = 16 + self.display_offset = False + self.display_hex = True + self.display_ascii = True + def render(data: Union[interfaces.renderers.LayerData, BaseAbsentValue]): if isinstance(data, BaseAbsentValue): # FIXME: Do something cleverer here return "" - data = data.context.layers[data.layer_name].read(data.offset, data.length) - return " ".join(f"{b:02x}" for b in data) + + layer = data.context.layers[data.layer_name] + # Map of the holes + error_bytes = set() + start_offset = data.offset - self.context_byte_len + end_offset = data.offset + data.length + self.context_byte_len + if isinstance(layer, interfaces.layers.TranslationLayerInterface): + error_bytes = set() + mapping = iter(layer.mapping(start_offset, end_offset, True)) + current_map = next(mapping) + for i in range(start_offset, end_offset): + # Run through the bytes, check if they're present + offset, sublength, _, _, _ = current_map + if i < offset: + error_bytes.add(i - start_offset) + if i > offset + sublength: + try: + current_map = next(mapping) + except StopIteration: + pass + offset, sublength, _, _, _ = current_map + if i > offset + sublength: + error_bytes.add(i - start_offset) + + # Padded data + specific_data = data.context.layers[data.layer_name].read( + start_offset, + end_offset - start_offset, + True, + ) + + printables = "" + output = "\n" + for count, byte in enumerate(specific_data): + output += f"{byte:02x} " + char = chr(byte) + printables += char if 0x20 <= byte <= 0x7E else "." + if count % self.width == self.width - 1: + output += printables + if count < len(specific_data) - 1: + output += "\n" + printables = "" + + # Handle leftovers when the length is not mutiple of width + if printables: + padding = self.width - len(printables) + output += " " * padding + output += printables + output += " " * padding + + return output render_func = render return super().__init__(render_func) From 1d5d981be180d2bdb19c643180d697e95a7a8385 Mon Sep 17 00:00:00 2001 From: Mike Auty Date: Sat, 15 Feb 2025 21:19:59 +0000 Subject: [PATCH 09/68] Windows: Convert malfind to LayerData renderer --- .../framework/plugins/windows/malfind.py | 43 ++++++++++++++++--- 1 file changed, 36 insertions(+), 7 deletions(-) diff --git a/volatility3/framework/plugins/windows/malfind.py b/volatility3/framework/plugins/windows/malfind.py index 33a20ee51..34ef9fb34 100644 --- a/volatility3/framework/plugins/windows/malfind.py +++ b/volatility3/framework/plugins/windows/malfind.py @@ -2,7 +2,7 @@ # which is available at https://www.volatilityfoundation.org/license/vsl-v1.0 # import logging -from typing import Iterable, Tuple +from typing import Iterable, Generator, Tuple from volatility3.framework import interfaces, symbols, exceptions from volatility3.framework import renderers @@ -88,6 +88,25 @@ class Malfind(interfaces.plugins.PluginInterface): symbol_table: str, proc: interfaces.objects.ObjectInterface, ) -> Iterable[Tuple[interfaces.objects.ObjectInterface, bytes]]: + for vad, data_object in cls.list_injection_sites( + context, kernel_layer_name, symbol_table, proc + ): + yield vad, data_object.context.layers[data_object.layer_name].read( + data_object.offset, data_object.length + ) + + @classmethod + def list_injection_sites( + cls, + context: interfaces.context.ContextInterface, + kernel_layer_name: str, + symbol_table: str, + proc: interfaces.objects.ObjectInterface, + ) -> Generator[ + Tuple[interfaces.objects.ObjectInterface, interfaces.renderers.LayerData], + None, + None, + ]: """Generate memory regions for a process that may contain injected code. @@ -156,8 +175,15 @@ class Malfind(interfaces.plugins.PluginInterface): vollog.warning( f"[proc_id {proc_id}] Found suspicious DIRTY + {protection_string} page at {hex(dirty_page)}", ) - data = proc_layer.read(vad.get_start(), 64, pad=True) - yield vad, data + start = vad.get_start() + length = 64 + data = interfaces.renderers.LayerData( + context=context, + layer_name=proc_layer_name, + offset=start, + length=length, + ) + yield (vad, data) def _generator(self, procs): # determine if we're on a 32 or 64 bit kernel @@ -166,7 +192,7 @@ class Malfind(interfaces.plugins.PluginInterface): # set refined criteria to know when to add to "Notes" column refined_criteria = { b"MZ": "MZ header", - b"\x55\x8B": "PE header", + b"\x55\x8b": "PE header", b"\x55\x48": "Function prologue", b"\x55\x89": "Function prologue", } @@ -179,11 +205,14 @@ class Malfind(interfaces.plugins.PluginInterface): # by default, "Notes" column will be set to N/A process_name = utility.array_to_string(proc.ImageFileName) - for vad, data in self.list_injections( + for vad, data_object in self.list_injection_sites( self.context, kernel.layer_name, kernel.symbol_table_name, proc ): notes = renderers.NotApplicableValue() # Check for unique headers and update "Notes" column if criteria is met + data = data_object.context.layers[data_object.layer_name].read( + data_object.offset, data_object.length, True + ) if data[0:2] in refined_criteria: notes = refined_criteria[data[0:2]] @@ -231,7 +260,7 @@ class Malfind(interfaces.plugins.PluginInterface): vad.get_private_memory(), file_output, notes, - format_hints.HexBytes(data), + data_object, disasm, ), ) @@ -251,7 +280,7 @@ class Malfind(interfaces.plugins.PluginInterface): ("PrivateMemory", int), ("File output", str), ("Notes", str), - ("Hexdump", format_hints.HexBytes), + ("Hexdump", interfaces.renderers.LayerData), ("Disasm", interfaces.renderers.Disassembly), ], self._generator( From 2304ea4cbe340fa9829e9eb6c034b0e1c359cf59 Mon Sep 17 00:00:00 2001 From: Mike Auty Date: Sat, 15 Feb 2025 21:44:55 +0000 Subject: [PATCH 10/68] Windows: Convert mftscan plugins to LayerData output --- volatility3/framework/plugins/windows/mftscan.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/volatility3/framework/plugins/windows/mftscan.py b/volatility3/framework/plugins/windows/mftscan.py index 2c5827a25..c6e6b8703 100644 --- a/volatility3/framework/plugins/windows/mftscan.py +++ b/volatility3/framework/plugins/windows/mftscan.py @@ -223,7 +223,7 @@ class MFTScan(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface): content = attr.get_resident_filecontent() if content: - content = format_hints.HexBytes(content) + content = interfaces.renderers.LayerData.from_object(content) else: content = renderers.NotAvailableValue() @@ -387,7 +387,7 @@ class ADS(interfaces.plugins.PluginInterface): ("MFT Type", str), ("Filename", str), ("ADS Filename", str), - ("Hexdump", format_hints.HexBytes), + ("Hexdump", interfaces.renderers.LayerData), ], self._generator(), ) @@ -453,7 +453,7 @@ class ResidentData(interfaces.plugins.PluginInterface): ("Record Number", int), ("MFT Type", str), ("Filename", str), - ("Hexdump", format_hints.HexBytes), + ("Hexdump", interfaces.renderers.LayerData), ], self._generator(), ) From 2e8d18e7cb7621821e724515ed23bf3c8320234d Mon Sep 17 00:00:00 2001 From: Mike Auty Date: Sat, 15 Feb 2025 21:51:14 +0000 Subject: [PATCH 11/68] Windows: Convert mbrscan over to LayerData output --- volatility3/framework/plugins/windows/mbrscan.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/volatility3/framework/plugins/windows/mbrscan.py b/volatility3/framework/plugins/windows/mbrscan.py index 4d5198181..b279307d7 100644 --- a/volatility3/framework/plugins/windows/mbrscan.py +++ b/volatility3/framework/plugins/windows/mbrscan.py @@ -149,7 +149,12 @@ class MBRScan(interfaces.plugins.PluginInterface): interfaces.renderers.Disassembly( bootcode, 0, architecture ), - format_hints.HexBytes(bootcode), + interfaces.renderers.LayerData( + context=self.context, + layer_name=layer.name, + offset=mbr_start_offset, + length=bootcode_length, + ), ), ) @@ -257,7 +262,7 @@ class MBRScan(interfaces.plugins.PluginInterface): ("EndingSector", int), ("SectorInSize", format_hints.Hex), ("Disasm", interfaces.renderers.Disassembly), - ("Bootcode", format_hints.HexBytes), + ("Bootcode", interfaces.renderers.LayerData), ], self._generator(), ) From e936b33784138e4f0916b61ea1edc555d2fd5f39 Mon Sep 17 00:00:00 2001 From: Mike Auty Date: Sat, 15 Feb 2025 21:52:15 +0000 Subject: [PATCH 12/68] CLI: Fix ruff check error --- volatility3/cli/text_renderer.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/volatility3/cli/text_renderer.py b/volatility3/cli/text_renderer.py index a0b45e353..99bb9fe73 100644 --- a/volatility3/cli/text_renderer.py +++ b/volatility3/cli/text_renderer.py @@ -1,5 +1,3 @@ -from volatility3.framework.interfaces.layers import TranslationLayerInterface - # This file is Copyright 2019 Volatility Foundation and licensed under the Volatility Software License 1.0 # which is available at https://www.volatilityfoundation.org/license/vsl-v1.0 # From 4145ef2c0d6e5760593a113054d5793c7798b614 Mon Sep 17 00:00:00 2001 From: Mike Auty Date: Sat, 15 Feb 2025 22:10:09 +0000 Subject: [PATCH 13/68] Renderers: Add no_surrounding to LayerData and include MINOR version bump --- volatility3/cli/text_renderer.py | 6 ++++-- volatility3/framework/constants/_version.py | 2 +- volatility3/framework/interfaces/renderers.py | 8 ++++++-- volatility3/framework/plugins/windows/malfind.py | 1 + volatility3/framework/plugins/windows/mbrscan.py | 1 + 5 files changed, 13 insertions(+), 5 deletions(-) diff --git a/volatility3/cli/text_renderer.py b/volatility3/cli/text_renderer.py index 99bb9fe73..7b78f4dda 100644 --- a/volatility3/cli/text_renderer.py +++ b/volatility3/cli/text_renderer.py @@ -161,11 +161,13 @@ class LayerDataRenderer(CLITypeRenderer): # FIXME: Do something cleverer here return "" + context_byte_len = self.context_byte_len if not data.no_context else 0 + layer = data.context.layers[data.layer_name] # Map of the holes error_bytes = set() - start_offset = data.offset - self.context_byte_len - end_offset = data.offset + data.length + self.context_byte_len + start_offset = data.offset - context_byte_len + end_offset = data.offset + data.length + context_byte_len if isinstance(layer, interfaces.layers.TranslationLayerInterface): error_bytes = set() mapping = iter(layer.mapping(start_offset, end_offset, True)) diff --git a/volatility3/framework/constants/_version.py b/volatility3/framework/constants/_version.py index f5da4c75b..64707b782 100644 --- a/volatility3/framework/constants/_version.py +++ b/volatility3/framework/constants/_version.py @@ -1,6 +1,6 @@ # We use the SemVer 2.0.0 versioning scheme VERSION_MAJOR = 2 # Number of releases of the library with a breaking change -VERSION_MINOR = 25 # Number of changes that only add to the interface +VERSION_MINOR = 26 # Number of changes that only add to the interface VERSION_PATCH = 0 # Number of changes that do not change the interface VERSION_SUFFIX = "" diff --git a/volatility3/framework/interfaces/renderers.py b/volatility3/framework/interfaces/renderers.py index e3fc02573..1034a211b 100644 --- a/volatility3/framework/interfaces/renderers.py +++ b/volatility3/framework/interfaces/renderers.py @@ -58,7 +58,7 @@ class TypeRendererInterface: def options(self): return self._options - def render(self, data: Union[T,BaseAbsentValue]) -> Any: + def render(self, data: Union[T, BaseAbsentValue]) -> Any: """Renders a specific datatype""" return "" @@ -166,16 +166,20 @@ class LayerData: layer_name: str offset: int length: int + no_surrounding: bool = False @staticmethod def from_object( - object: "interfaces.objects.ObjectInterface", size: Optional[int] = None + object: "interfaces.objects.ObjectInterface", + size: Optional[int] = None, + no_surrounding: bool = True, ): return LayerData( context=object._context, layer_name=object.vol.layer_name, offset=object.vol.offset, length=size or object.vol.size, + no_surrounding=no_surrounding, ) diff --git a/volatility3/framework/plugins/windows/malfind.py b/volatility3/framework/plugins/windows/malfind.py index 34ef9fb34..57ecbb062 100644 --- a/volatility3/framework/plugins/windows/malfind.py +++ b/volatility3/framework/plugins/windows/malfind.py @@ -182,6 +182,7 @@ class Malfind(interfaces.plugins.PluginInterface): layer_name=proc_layer_name, offset=start, length=length, + no_surrounding=True, ) yield (vad, data) diff --git a/volatility3/framework/plugins/windows/mbrscan.py b/volatility3/framework/plugins/windows/mbrscan.py index b279307d7..64cbdfc9d 100644 --- a/volatility3/framework/plugins/windows/mbrscan.py +++ b/volatility3/framework/plugins/windows/mbrscan.py @@ -154,6 +154,7 @@ class MBRScan(interfaces.plugins.PluginInterface): layer_name=layer.name, offset=mbr_start_offset, length=bootcode_length, + no_surrounding=True, ), ), ) From 85870a9a9427bd0cc6fdce598e6a47530612f235 Mon Sep 17 00:00:00 2001 From: Mike Auty Date: Sat, 15 Feb 2025 22:14:22 +0000 Subject: [PATCH 14/68] Windows: Update the required framework version for plugins outputting LayerData --- volatility3/framework/plugins/windows/malfind.py | 2 +- volatility3/framework/plugins/windows/mbrscan.py | 2 +- volatility3/framework/plugins/windows/mftscan.py | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/volatility3/framework/plugins/windows/malfind.py b/volatility3/framework/plugins/windows/malfind.py index 57ecbb062..b04c21f52 100644 --- a/volatility3/framework/plugins/windows/malfind.py +++ b/volatility3/framework/plugins/windows/malfind.py @@ -17,7 +17,7 @@ vollog = logging.getLogger(__name__) class Malfind(interfaces.plugins.PluginInterface): """Lists process memory ranges that potentially contain injected code.""" - _required_framework_version = (2, 4, 0) + _required_framework_version = (2, 22, 0) @classmethod def get_requirements(cls): diff --git a/volatility3/framework/plugins/windows/mbrscan.py b/volatility3/framework/plugins/windows/mbrscan.py index 64cbdfc9d..c0db350fe 100644 --- a/volatility3/framework/plugins/windows/mbrscan.py +++ b/volatility3/framework/plugins/windows/mbrscan.py @@ -20,7 +20,7 @@ vollog = logging.getLogger(__name__) class MBRScan(interfaces.plugins.PluginInterface): """Scans for and parses potential Master Boot Records (MBRs)""" - _required_framework_version = (2, 0, 1) + _required_framework_version = (2, 22, 0) _version = (1, 0, 0) @classmethod diff --git a/volatility3/framework/plugins/windows/mftscan.py b/volatility3/framework/plugins/windows/mftscan.py index c6e6b8703..6f07effd6 100644 --- a/volatility3/framework/plugins/windows/mftscan.py +++ b/volatility3/framework/plugins/windows/mftscan.py @@ -326,7 +326,7 @@ class MFTScan(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface): class ADS(interfaces.plugins.PluginInterface): """Scans for Alternate Data Stream""" - _required_framework_version = (2, 7, 0) + _required_framework_version = (2, 22, 0) _version = (1, 0, 1) @@ -396,7 +396,7 @@ class ADS(interfaces.plugins.PluginInterface): class ResidentData(interfaces.plugins.PluginInterface): """Scans for MFT Records with Resident Data""" - _required_framework_version = (2, 7, 0) + _required_framework_version = (2, 22, 0) _version = (1, 0, 1) From 94d6f4f1313a57a539844b7b9f85f3633dfd09e3 Mon Sep 17 00:00:00 2001 From: Mike Auty Date: Sat, 15 Feb 2025 22:27:10 +0000 Subject: [PATCH 15/68] CLI: Indicate missing bytes from padded bytes --- volatility3/cli/text_renderer.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/volatility3/cli/text_renderer.py b/volatility3/cli/text_renderer.py index 7b78f4dda..21816f866 100644 --- a/volatility3/cli/text_renderer.py +++ b/volatility3/cli/text_renderer.py @@ -196,9 +196,13 @@ class LayerDataRenderer(CLITypeRenderer): printables = "" output = "\n" for count, byte in enumerate(specific_data): - output += f"{byte:02x} " - char = chr(byte) - printables += char if 0x20 <= byte <= 0x7E else "." + if count not in error_bytes: + output += f"{byte:02x} " + char = chr(byte) + printables += char if 0x20 <= byte <= 0x7E else "." + else: + output += "__ " + printables += "." if count % self.width == self.width - 1: output += printables if count < len(specific_data) - 1: From 6af8cd09f07f5bf4331885f482ffacde3470a72c Mon Sep 17 00:00:00 2001 From: Mike Auty Date: Sun, 16 Feb 2025 16:12:14 +0000 Subject: [PATCH 16/68] Renderers: Add in fallback method for formatting cellrenderers --- volatility3/framework/interfaces/renderers.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/volatility3/framework/interfaces/renderers.py b/volatility3/framework/interfaces/renderers.py index 1034a211b..618364bdf 100644 --- a/volatility3/framework/interfaces/renderers.py +++ b/volatility3/framework/interfaces/renderers.py @@ -153,6 +153,10 @@ class Disassembly: raise TypeError("Offset must be an integer type") self.offset = offset + def __str__(self) -> str: + """Fallback method of rendering""" + return str(self.data) + @dataclasses.dataclass class LayerData: @@ -182,6 +186,11 @@ class LayerData: no_surrounding=no_surrounding, ) + def __str__(self) -> str: + """Fallback method of rendering""" + data = self.context.layers[self.layer_name].read(self.offset, self.length, True) + return str(data) + # We don't class these off a shared base, because the BaseTypes must only # contain the types that the validator will accept (which would not include the base) From c18c6cbf3038454f9a67fe243af23702cfa28d49 Mon Sep 17 00:00:00 2001 From: Mike Auty Date: Sun, 16 Feb 2025 16:39:53 +0000 Subject: [PATCH 17/68] Core: Shift renderers from interfaces --- volatility3/cli/text_renderer.py | 12 +-- volatility3/framework/interfaces/renderers.py | 65 +++++----------- .../framework/plugins/linux/malfind.py | 8 +- volatility3/framework/plugins/mac/malfind.py | 6 +- .../framework/plugins/windows/malfind.py | 12 ++- .../framework/plugins/windows/mbrscan.py | 18 ++--- .../framework/plugins/windows/mftscan.py | 6 +- volatility3/framework/renderers/__init__.py | 77 +++++++++++++++++++ 8 files changed, 121 insertions(+), 83 deletions(-) diff --git a/volatility3/cli/text_renderer.py b/volatility3/cli/text_renderer.py index 21816f866..1d39e3fa6 100644 --- a/volatility3/cli/text_renderer.py +++ b/volatility3/cli/text_renderer.py @@ -114,7 +114,7 @@ def quoted_optional(func: Callable) -> Callable: return wrapped -def display_disassembly(disasm: interfaces.renderers.Disassembly) -> str: +def display_disassembly(disasm: renderers.Disassembly) -> str: """Renders a disassembly renderer type into string format. Args: @@ -156,12 +156,12 @@ class LayerDataRenderer(CLITypeRenderer): self.display_hex = True self.display_ascii = True - def render(data: Union[interfaces.renderers.LayerData, BaseAbsentValue]): + def render(data: Union[renderers.LayerData, BaseAbsentValue]): if isinstance(data, BaseAbsentValue): # FIXME: Do something cleverer here return "" - context_byte_len = self.context_byte_len if not data.no_context else 0 + context_byte_len = self.context_byte_len if not data.no_surrounding else 0 layer = data.context.layers[data.layer_name] # Map of the holes @@ -230,9 +230,9 @@ class CLIRenderer(interfaces.renderers.Renderer): format_hints.Hex: CLITypeRenderer(lambda x: f"0x{x:x}"), format_hints.HexBytes: CLITypeRenderer(hex_bytes_as_text), format_hints.MultiTypeData: CLITypeRenderer(multitypedata_as_text), - interfaces.renderers.Disassembly: CLITypeRenderer(display_disassembly), + renderers.Disassembly: CLITypeRenderer(display_disassembly), bytes: CLITypeRenderer(lambda x: " ".join(f"{b:02x}" for b in x)), - interfaces.renderers.LayerData: LayerDataRenderer(), + renderers.LayerData: LayerDataRenderer(), datetime.datetime: CLITypeRenderer( lambda x: x.strftime("%Y-%m-%d %H:%M:%S.%f %Z") ), @@ -523,7 +523,7 @@ class PrettyTextRenderer(CLIRenderer): class JsonRenderer(CLIRenderer): _type_renderers = { format_hints.HexBytes: quoted_optional(hex_bytes_as_text), - interfaces.renderers.Disassembly: quoted_optional(display_disassembly), + renderers.Disassembly: quoted_optional(display_disassembly), format_hints.MultiTypeData: quoted_optional(multitypedata_as_text), bytes: optional(lambda x: " ".join(f"{b:02x}" for b in x)), datetime.datetime: lambda x: ( diff --git a/volatility3/framework/interfaces/renderers.py b/volatility3/framework/interfaces/renderers.py index 618364bdf..b4c93cb3e 100644 --- a/volatility3/framework/interfaces/renderers.py +++ b/volatility3/framework/interfaces/renderers.py @@ -9,8 +9,8 @@ renderer interface which can interact with a TreeGrid to produce suitable output. """ -import dataclasses import datetime +import warnings from abc import ABCMeta, abstractmethod from collections import abc from typing import ( @@ -31,9 +31,19 @@ from typing import Dict from volatility3.framework import interfaces +class BasicType: + def __str__(self) -> str: + """Fallback method for rendering basic types""" + return str(self) + + class BaseAbsentValue: """Class that represents values which are not present for some reason.""" + def __str__(self) -> str: + """Fallback method for rendering basic types""" + return "-" + class Column(NamedTuple): name: str @@ -136,7 +146,7 @@ class TreeNode(abc.Sequence, metaclass=ABCMeta): """ -class Disassembly: +class Disassembly(BasicType): """A class to indicate that the bytes provided should be disassembled (based on the architecture)""" @@ -145,6 +155,10 @@ class Disassembly: def __init__( self, data: bytes, offset: int = 0, architecture: str = "intel64" ) -> None: + warnings.warn( + f"interfaces.renderers.Disassembly is now renderers.Disassembly", + FutureWarning, + ) self.data = data self.architecture = None if architecture in self.possible_architectures: @@ -158,40 +172,6 @@ class Disassembly: return str(self.data) -@dataclasses.dataclass -class LayerData: - """Layer data - - This requires the contex to be passed in, in case plugins want to use multiple contexts - and to ensure the TreeGrid interface doesn't change, since this would break all existing plugins - """ - - context: "interfaces.context.ContextInterface" - layer_name: str - offset: int - length: int - no_surrounding: bool = False - - @staticmethod - def from_object( - object: "interfaces.objects.ObjectInterface", - size: Optional[int] = None, - no_surrounding: bool = True, - ): - return LayerData( - context=object._context, - layer_name=object.vol.layer_name, - offset=object.vol.offset, - length=size or object.vol.size, - no_surrounding=no_surrounding, - ) - - def __str__(self) -> str: - """Fallback method of rendering""" - data = self.context.layers[self.layer_name].read(self.offset, self.length, True) - return str(data) - - # We don't class these off a shared base, because the BaseTypes must only # contain the types that the validator will accept (which would not include the base) @@ -203,8 +183,7 @@ BaseTypes = Union[ Type[bytes], Type[datetime.datetime], Type[BaseAbsentValue], - Type[Disassembly], - Type[LayerData], + Type[BasicType], ] ColumnsType = List[Tuple[str, BaseTypes]] VisitorSignature = Callable[[TreeNode, _Type], _Type] @@ -224,15 +203,7 @@ class TreeGrid(metaclass=ABCMeta): """ # TODO: Figure out why this isn't just BaseTypes (which includes AbsentValues' - base_types: ClassVar[Tuple] = ( - int, - str, - float, - bytes, - datetime.datetime, - Disassembly, - LayerData, - ) + base_types: ClassVar[Tuple] = (int, str, float, bytes, datetime.datetime, BasicType) def __init__( self, diff --git a/volatility3/framework/plugins/linux/malfind.py b/volatility3/framework/plugins/linux/malfind.py index 8bbf3b89c..663f83bd5 100644 --- a/volatility3/framework/plugins/linux/malfind.py +++ b/volatility3/framework/plugins/linux/malfind.py @@ -18,7 +18,7 @@ class Malfind(interfaces.plugins.PluginInterface): """Lists process memory ranges that potentially contain injected code.""" _required_framework_version = (2, 0, 0) - _version = (1, 0, 2) + _version = (1, 0, 3) @classmethod def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]: @@ -76,9 +76,7 @@ class Malfind(interfaces.plugins.PluginInterface): else: architecture = "intel64" - disasm = interfaces.renderers.Disassembly( - data, vma.vm_start, architecture - ) + disasm = renderers.Disassembly(data, vma.vm_start, architecture) yield ( 0, @@ -106,7 +104,7 @@ class Malfind(interfaces.plugins.PluginInterface): ("Path", str), ("Protection", str), ("Hexdump", format_hints.HexBytes), - ("Disasm", interfaces.renderers.Disassembly), + ("Disasm", renderers.Disassembly), ], self._generator( pslist.PsList.list_tasks( diff --git a/volatility3/framework/plugins/mac/malfind.py b/volatility3/framework/plugins/mac/malfind.py index 3094ada85..f1c3cc409 100644 --- a/volatility3/framework/plugins/mac/malfind.py +++ b/volatility3/framework/plugins/mac/malfind.py @@ -68,9 +68,7 @@ class Malfind(interfaces.plugins.PluginInterface): else: architecture = "intel64" - disasm = interfaces.renderers.Disassembly( - data, vma.links.start, architecture - ) + disasm = renderers.Disassembly(data, vma.links.start, architecture) yield ( 0, @@ -99,7 +97,7 @@ class Malfind(interfaces.plugins.PluginInterface): ("End", format_hints.Hex), ("Protection", str), ("Hexdump", format_hints.HexBytes), - ("Disasm", interfaces.renderers.Disassembly), + ("Disasm", renderers.Disassembly), ], self._generator( list_tasks(self.context, self.config["kernel"], filter_func=filter_func) diff --git a/volatility3/framework/plugins/windows/malfind.py b/volatility3/framework/plugins/windows/malfind.py index b04c21f52..a91492049 100644 --- a/volatility3/framework/plugins/windows/malfind.py +++ b/volatility3/framework/plugins/windows/malfind.py @@ -103,7 +103,7 @@ class Malfind(interfaces.plugins.PluginInterface): symbol_table: str, proc: interfaces.objects.ObjectInterface, ) -> Generator[ - Tuple[interfaces.objects.ObjectInterface, interfaces.renderers.LayerData], + Tuple[interfaces.objects.ObjectInterface, renderers.LayerData], None, None, ]: @@ -177,7 +177,7 @@ class Malfind(interfaces.plugins.PluginInterface): ) start = vad.get_start() length = 64 - data = interfaces.renderers.LayerData( + data = renderers.LayerData( context=context, layer_name=proc_layer_name, offset=start, @@ -223,9 +223,7 @@ class Malfind(interfaces.plugins.PluginInterface): else: architecture = "intel64" - disasm = interfaces.renderers.Disassembly( - data, vad.get_start(), architecture - ) + disasm = renderers.Disassembly(data, vad.get_start(), architecture) file_output = "Disabled" if self.config["dump"]: @@ -281,8 +279,8 @@ class Malfind(interfaces.plugins.PluginInterface): ("PrivateMemory", int), ("File output", str), ("Notes", str), - ("Hexdump", interfaces.renderers.LayerData), - ("Disasm", interfaces.renderers.Disassembly), + ("Hexdump", renderers.LayerData), + ("Disasm", renderers.Disassembly), ], self._generator( pslist.PsList.list_processes( diff --git a/volatility3/framework/plugins/windows/mbrscan.py b/volatility3/framework/plugins/windows/mbrscan.py index c0db350fe..aac3001c5 100644 --- a/volatility3/framework/plugins/windows/mbrscan.py +++ b/volatility3/framework/plugins/windows/mbrscan.py @@ -74,7 +74,7 @@ class MBRScan(interfaces.plugins.PluginInterface): partition_table_object = symbol_table + constants.BANG + "PARTITION_TABLE" # Define Signature and Data Length - mbr_signature = b"\x55\xAA" + mbr_signature = b"\x55\xaa" mbr_length = 0x200 bootcode_length = 0x1B8 @@ -120,9 +120,7 @@ class MBRScan(interfaces.plugins.PluginInterface): renderers.NotApplicableValue(), renderers.NotApplicableValue(), renderers.NotApplicableValue(), - interfaces.renderers.Disassembly( - bootcode, 0, architecture - ), + renderers.Disassembly(bootcode, 0, architecture), ), ) else: @@ -146,10 +144,8 @@ class MBRScan(interfaces.plugins.PluginInterface): renderers.NotApplicableValue(), renderers.NotApplicableValue(), renderers.NotApplicableValue(), - interfaces.renderers.Disassembly( - bootcode, 0, architecture - ), - interfaces.renderers.LayerData( + renderers.Disassembly(bootcode, 0, architecture), + renderers.LayerData( context=self.context, layer_name=layer.name, offset=mbr_start_offset, @@ -238,7 +234,7 @@ class MBRScan(interfaces.plugins.PluginInterface): ("Bootable", bool), ("PartitionType", str), ("SectorInSize", format_hints.Hex), - ("Disasm", interfaces.renderers.Disassembly), + ("Disasm", renderers.Disassembly), ], self._generator(), ) @@ -262,8 +258,8 @@ class MBRScan(interfaces.plugins.PluginInterface): ("EndingCHS", int), ("EndingSector", int), ("SectorInSize", format_hints.Hex), - ("Disasm", interfaces.renderers.Disassembly), - ("Bootcode", interfaces.renderers.LayerData), + ("Disasm", renderers.Disassembly), + ("Bootcode", renderers.LayerData), ], self._generator(), ) diff --git a/volatility3/framework/plugins/windows/mftscan.py b/volatility3/framework/plugins/windows/mftscan.py index 6f07effd6..8ba110169 100644 --- a/volatility3/framework/plugins/windows/mftscan.py +++ b/volatility3/framework/plugins/windows/mftscan.py @@ -223,7 +223,7 @@ class MFTScan(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface): content = attr.get_resident_filecontent() if content: - content = interfaces.renderers.LayerData.from_object(content) + content = renderers.LayerData.from_object(content) else: content = renderers.NotAvailableValue() @@ -387,7 +387,7 @@ class ADS(interfaces.plugins.PluginInterface): ("MFT Type", str), ("Filename", str), ("ADS Filename", str), - ("Hexdump", interfaces.renderers.LayerData), + ("Hexdump", renderers.LayerData), ], self._generator(), ) @@ -453,7 +453,7 @@ class ResidentData(interfaces.plugins.PluginInterface): ("Record Number", int), ("MFT Type", str), ("Filename", str), - ("Hexdump", interfaces.renderers.LayerData), + ("Hexdump", renderers.LayerData), ], self._generator(), ) diff --git a/volatility3/framework/renderers/__init__.py b/volatility3/framework/renderers/__init__.py index 093edf8cc..4f1de586a 100644 --- a/volatility3/framework/renderers/__init__.py +++ b/volatility3/framework/renderers/__init__.py @@ -8,6 +8,7 @@ or file or graphical output """ import collections import collections.abc +import dataclasses import datetime import logging from typing import Any, Callable, Dict, Iterable, List, Optional, Tuple, TypeVar, Union @@ -22,16 +23,28 @@ class UnreadableValue(interfaces.renderers.BaseAbsentValue): """Class that represents values which are empty because the data cannot be read.""" + def __str__(self) -> str: + """Fallback method for rendering basic types""" + return "-" + class UnparsableValue(interfaces.renderers.BaseAbsentValue): """Class that represents values which are empty because the data cannot be interpreted correctly.""" + def __str__(self) -> str: + """Fallback method for rendering basic types""" + return "-" + class NotApplicableValue(interfaces.renderers.BaseAbsentValue): """Class that represents values which are empty because they don't make sense for this node.""" + def __str__(self) -> str: + """Fallback method for rendering basic types""" + return "N/A" + class NotAvailableValue(interfaces.renderers.BaseAbsentValue): """Class that represents values which cannot be provided now (but might in @@ -45,6 +58,70 @@ class NotAvailableValue(interfaces.renderers.BaseAbsentValue): in preference, and only if neither fits should this be used. """ + def __str__(self) -> str: + """Fallback method for rendering basic types""" + return "N/A" + + +########## +### Basic Types + + +class Disassembly(interfaces.renderers.BasicType): + """A class to indicate that the bytes provided should be disassembled + (based on the architecture)""" + + possible_architectures = ["intel", "intel64", "arm", "arm64"] + + def __init__( + self, data: bytes, offset: int = 0, architecture: str = "intel64" + ) -> None: + self.data = data + self.architecture = None + if architecture in self.possible_architectures: + self.architecture = architecture + if not isinstance(offset, int): + raise TypeError("Offset must be an integer type") + self.offset = offset + + def __str__(self) -> str: + """Fallback method of rendering""" + return str(self.data) + + +@dataclasses.dataclass +class LayerData(interfaces.renderers.BasicType): + """Layer data + + This requires the contex to be passed in, in case plugins want to use multiple contexts + and to ensure the TreeGrid interface doesn't change, since this would break all existing plugins + """ + + context: "interfaces.context.ContextInterface" + layer_name: str + offset: int + length: int + no_surrounding: bool = False + + @staticmethod + def from_object( + object: "interfaces.objects.ObjectInterface", + size: Optional[int] = None, + no_surrounding: bool = True, + ): + return LayerData( + context=object._context, + layer_name=object.vol.layer_name, + offset=object.vol.offset, + length=size or object.vol.size, + no_surrounding=no_surrounding, + ) + + def __str__(self) -> str: + """Fallback method of rendering""" + data = self.context.layers[self.layer_name].read(self.offset, self.length, True) + return str(data) + class TreeNode(interfaces.renderers.TreeNode): """Class representing a particular node in a tree grid.""" From af01fcdcffa4c52e195de6df4ed7a6aa48bfea51 Mon Sep 17 00:00:00 2001 From: Mike Auty Date: Sun, 16 Feb 2025 17:41:28 +0000 Subject: [PATCH 18/68] Core: Remove unnecessary f-string --- volatility3/framework/interfaces/renderers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/volatility3/framework/interfaces/renderers.py b/volatility3/framework/interfaces/renderers.py index b4c93cb3e..3e9afaf21 100644 --- a/volatility3/framework/interfaces/renderers.py +++ b/volatility3/framework/interfaces/renderers.py @@ -156,7 +156,7 @@ class Disassembly(BasicType): self, data: bytes, offset: int = 0, architecture: str = "intel64" ) -> None: warnings.warn( - f"interfaces.renderers.Disassembly is now renderers.Disassembly", + "interfaces.renderers.Disassembly is now renderers.Disassembly", FutureWarning, ) self.data = data From 6f599fa6456f812c7d6028588e987550c35bf86e Mon Sep 17 00:00:00 2001 From: Mike Auty Date: Fri, 21 Feb 2025 23:53:59 +0000 Subject: [PATCH 19/68] Various: Minor bump malfind functions for all OSes --- volatility3/framework/plugins/mac/malfind.py | 2 +- volatility3/framework/plugins/windows/malfind.py | 1 + volatility3/framework/plugins/windows/mbrscan.py | 2 +- volatility3/framework/plugins/windows/mftscan.py | 4 ++-- 4 files changed, 5 insertions(+), 4 deletions(-) diff --git a/volatility3/framework/plugins/mac/malfind.py b/volatility3/framework/plugins/mac/malfind.py index f1c3cc409..2c2c801dc 100644 --- a/volatility3/framework/plugins/mac/malfind.py +++ b/volatility3/framework/plugins/mac/malfind.py @@ -13,7 +13,7 @@ from volatility3.plugins.mac import pslist class Malfind(interfaces.plugins.PluginInterface): """Lists process memory ranges that potentially contain injected code.""" - _required_framework_version = (2, 0, 0) + _required_framework_version = (2, 0, 1) @classmethod def get_requirements(cls): diff --git a/volatility3/framework/plugins/windows/malfind.py b/volatility3/framework/plugins/windows/malfind.py index a91492049..d9861d9c8 100644 --- a/volatility3/framework/plugins/windows/malfind.py +++ b/volatility3/framework/plugins/windows/malfind.py @@ -18,6 +18,7 @@ class Malfind(interfaces.plugins.PluginInterface): """Lists process memory ranges that potentially contain injected code.""" _required_framework_version = (2, 22, 0) + _version = (1, 1, 0) @classmethod def get_requirements(cls): diff --git a/volatility3/framework/plugins/windows/mbrscan.py b/volatility3/framework/plugins/windows/mbrscan.py index aac3001c5..775b1a894 100644 --- a/volatility3/framework/plugins/windows/mbrscan.py +++ b/volatility3/framework/plugins/windows/mbrscan.py @@ -21,7 +21,7 @@ class MBRScan(interfaces.plugins.PluginInterface): """Scans for and parses potential Master Boot Records (MBRs)""" _required_framework_version = (2, 22, 0) - _version = (1, 0, 0) + _version = (1, 0, 1) @classmethod def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]: diff --git a/volatility3/framework/plugins/windows/mftscan.py b/volatility3/framework/plugins/windows/mftscan.py index 8ba110169..4139aab60 100644 --- a/volatility3/framework/plugins/windows/mftscan.py +++ b/volatility3/framework/plugins/windows/mftscan.py @@ -328,7 +328,7 @@ class ADS(interfaces.plugins.PluginInterface): _required_framework_version = (2, 22, 0) - _version = (1, 0, 1) + _version = (1, 0, 2) @classmethod def get_requirements(cls): @@ -398,7 +398,7 @@ class ResidentData(interfaces.plugins.PluginInterface): _required_framework_version = (2, 22, 0) - _version = (1, 0, 1) + _version = (1, 0, 2) @classmethod def get_requirements(cls): From e86981c8806f64f4db5a7297cdd0b3fd4a5045ea Mon Sep 17 00:00:00 2001 From: Mike Auty Date: Sat, 22 Feb 2025 00:02:55 +0000 Subject: [PATCH 20/68] Various: Update yarascan plugins to output LayerData instead of just bytes --- volatility3/framework/plugins/linux/vmayarascan.py | 14 ++++++++++---- .../framework/plugins/windows/vadyarascan.py | 14 ++++++++++---- volatility3/framework/plugins/yarascan.py | 14 ++++++++++---- 3 files changed, 30 insertions(+), 12 deletions(-) diff --git a/volatility3/framework/plugins/linux/vmayarascan.py b/volatility3/framework/plugins/linux/vmayarascan.py index e9e56dd0f..42fd8e375 100644 --- a/volatility3/framework/plugins/linux/vmayarascan.py +++ b/volatility3/framework/plugins/linux/vmayarascan.py @@ -17,8 +17,8 @@ vollog = logging.getLogger(__name__) class VmaYaraScan(interfaces.plugins.PluginInterface): """Scans all virtual memory areas for tasks using yara.""" - _required_framework_version = (2, 4, 0) - _version = (1, 0, 3) + _required_framework_version = (2, 22, 0) + _version = (1, 0, 4) @classmethod def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]: @@ -97,12 +97,18 @@ class VmaYaraScan(interfaces.plugins.PluginInterface): 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, - value, + layer_data, ) @classmethod @@ -130,7 +136,7 @@ class VmaYaraScan(interfaces.plugins.PluginInterface): ("PID", int), ("Rule", str), ("Component", str), - ("Value", bytes), + ("Value", renderers.LayerData), ], self._generator(), ) diff --git a/volatility3/framework/plugins/windows/vadyarascan.py b/volatility3/framework/plugins/windows/vadyarascan.py index a19206e22..e18f435db 100644 --- a/volatility3/framework/plugins/windows/vadyarascan.py +++ b/volatility3/framework/plugins/windows/vadyarascan.py @@ -17,8 +17,8 @@ vollog = logging.getLogger(__name__) class VadYaraScan(interfaces.plugins.PluginInterface): """Scans all the Virtual Address Descriptor memory maps using yara.""" - _required_framework_version = (2, 4, 0) - _version = (1, 1, 2) + _required_framework_version = (2, 22, 0) + _version = (1, 1, 3) @classmethod def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]: @@ -93,12 +93,18 @@ class VadYaraScan(interfaces.plugins.PluginInterface): for offset, rule_name, name, value in scanner( layer.read(start, size, pad=True), start ): + layer_data = renderers.LayerData( + context=self.context, + offset=offset, + layer_name=layer.name, + length=len(value), + ) yield 0, ( format_hints.Hex(offset), task.UniqueProcessId, rule_name, name, - value, + layer_data, ) @classmethod @@ -126,7 +132,7 @@ class VadYaraScan(interfaces.plugins.PluginInterface): ("PID", int), ("Rule", str), ("Component", str), - ("Value", bytes), + ("Value", renderers.LayerData), ], self._generator(), ) diff --git a/volatility3/framework/plugins/yarascan.py b/volatility3/framework/plugins/yarascan.py index 38c8b6085..df31b18d7 100644 --- a/volatility3/framework/plugins/yarascan.py +++ b/volatility3/framework/plugins/yarascan.py @@ -105,8 +105,8 @@ class YaraScanner(interfaces.layers.ScannerInterface): class YaraScan(plugins.PluginInterface): """Scans kernel memory using yara rules (string or file).""" - _required_framework_version = (2, 0, 0) - _version = (2, 0, 0) + _required_framework_version = (2, 22, 0) + _version = (2, 0, 1) _yara_x = USE_YARA_X @classmethod @@ -201,7 +201,13 @@ class YaraScan(plugins.PluginInterface): for offset, rule_name, name, value in layer.scan( context=self.context, scanner=YaraScanner(rules=rules) ): - yield 0, (format_hints.Hex(offset), rule_name, name, value) + layer_data = renderers.LayerData( + context=self.context, + offset=offset, + layer_name=layer.name, + length=len(value), + ) + yield 0, (format_hints.Hex(offset), rule_name, name, layer_data) def run(self): return renderers.TreeGrid( @@ -209,7 +215,7 @@ class YaraScan(plugins.PluginInterface): ("Offset", format_hints.Hex), ("Rule", str), ("Component", str), - ("Value", bytes), + ("Value", renderers.LayerData), ], self._generator(), ) From f0153817c5bcbb72093c1db70e79023405db1144 Mon Sep 17 00:00:00 2001 From: Mike Auty Date: Mon, 24 Mar 2025 00:58:28 +0000 Subject: [PATCH 21/68] Add in slots to object model --- volatility3/framework/interfaces/objects.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/volatility3/framework/interfaces/objects.py b/volatility3/framework/interfaces/objects.py index 62c31481b..7317469f1 100644 --- a/volatility3/framework/interfaces/objects.py +++ b/volatility3/framework/interfaces/objects.py @@ -23,6 +23,8 @@ class ReadOnlyMapping(collections.abc.Mapping): modified, making an immutable mapping. """ + __slots__ = ("_dict",) + def __init__(self, dictionary: Mapping[str, Any]) -> None: self._dict = dictionary @@ -63,6 +65,8 @@ class ObjectInformation(ReadOnlyMapping): in a single place. These values are based on the :class:`ReadOnlyMapping` class, to prevent their modification. """ + __slots__ = () + def __init__( self, layer_name: str, @@ -98,6 +102,8 @@ class ObjectInterface(metaclass=abc.ABCMeta): """A base object required to be the ancestor of every object used in volatility.""" + __slots__ = () + def __init__( self, context: "interfaces.context.ContextInterface", @@ -305,6 +311,8 @@ class Template: constructed at resolution time and then cached. """ + __slots__ = "_vol" + def __init__(self, type_name: str, **arguments) -> None: """Stores the keyword arguments for later object creation.""" # Allow the updating of template arguments whilst still in template form From 3153cd7e30dd7444eb6e35be9f4ed35bc4febc8f Mon Sep 17 00:00:00 2001 From: Mike Auty Date: Mon, 24 Mar 2025 19:21:51 +0000 Subject: [PATCH 22/68] Remove the chainmap and multiple dictionaries to reduce memory consumption --- volatility3/framework/interfaces/objects.py | 12 ++++++------ volatility3/framework/renderers/__init__.py | 7 +++++-- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/volatility3/framework/interfaces/objects.py b/volatility3/framework/interfaces/objects.py index 7317469f1..8419f9d4d 100644 --- a/volatility3/framework/interfaces/objects.py +++ b/volatility3/framework/interfaces/objects.py @@ -133,8 +133,10 @@ class ObjectInterface(metaclass=abc.ABCMeta): mask = context.layers[object_info.layer_name].address_mask normalized_offset = object_info.offset & mask + self._vol = kwargs vol_info_dict = {"type_name": type_name, "offset": normalized_offset} - self._vol = collections.ChainMap({}, vol_info_dict, object_info, kwargs) + self._vol.update(object_info) + self._vol.update(vol_info_dict) self._context = context def __getattr__(self, attr: str) -> Any: @@ -317,10 +319,8 @@ class Template: """Stores the keyword arguments for later object creation.""" # Allow the updating of template arguments whilst still in template form super().__init__() - empty_dict: Dict[str, Any] = {} - self._vol = collections.ChainMap( - empty_dict, arguments, {"type_name": type_name} - ) + self._vol = {"type_name": type_name} + self._vol.update(arguments) @property def vol(self) -> ReadOnlyMapping: @@ -364,7 +364,7 @@ class Template: def clone(self) -> "Template": """Returns a copy of the original Template as constructed (without `update_vol` additions having been made)""" - clone = self.__class__(**self._vol.parents.new_child()) + clone = self.__class__(**self._vol) return clone def update_vol(self, **new_arguments) -> None: diff --git a/volatility3/framework/renderers/__init__.py b/volatility3/framework/renderers/__init__.py index 093edf8cc..cc3129e87 100644 --- a/volatility3/framework/renderers/__init__.py +++ b/volatility3/framework/renderers/__init__.py @@ -61,7 +61,7 @@ class TreeNode(interfaces.renderers.TreeNode): self._treegrid = treegrid self._parent = parent self._path = path - self._validate_values(values) + validated_values = self._validate_values(values) self._values = treegrid.RowStructure(*values) # type: ignore def __repr__(self) -> str: @@ -73,9 +73,12 @@ class TreeNode(interfaces.renderers.TreeNode): def __len__(self) -> int: return len(self._treegrid.children(self)) - def _validate_values(self, values: List[interfaces.renderers.BaseTypes]) -> None: + def _validate_values( + self, values: List[interfaces.renderers.BaseTypes] + ) -> List[interfaces.renderers.BaseTypes]: """A function for raising exceptions if a given set of values is invalid according to the column properties.""" + new_values = () if not ( isinstance(values, collections.abc.Sequence) and len(values) == len(self._treegrid.columns) From 8610c681aba380b7f77ddcce9ed22e716e10b7f5 Mon Sep 17 00:00:00 2001 From: Mike Auty Date: Mon, 24 Mar 2025 22:33:25 +0000 Subject: [PATCH 23/68] Restore the chainmap, since we need it for cloning --- volatility3/framework/interfaces/objects.py | 50 ++++++++++++++++----- volatility3/framework/renderers/__init__.py | 7 +-- 2 files changed, 41 insertions(+), 16 deletions(-) diff --git a/volatility3/framework/interfaces/objects.py b/volatility3/framework/interfaces/objects.py index 8419f9d4d..995a5f29b 100644 --- a/volatility3/framework/interfaces/objects.py +++ b/volatility3/framework/interfaces/objects.py @@ -54,7 +54,7 @@ class ReadOnlyMapping(collections.abc.Mapping): return dict(self) == dict(other) -class ObjectInformation(ReadOnlyMapping): +class ObjectInformation(collections.abc.Mapping): """Contains common information useful/pertinent only to an individual object (like an instance) @@ -65,7 +65,14 @@ class ObjectInformation(ReadOnlyMapping): in a single place. These values are based on the :class:`ReadOnlyMapping` class, to prevent their modification. """ - __slots__ = () + __slots__ = ( + "layer_name", + "offset", + "member_name", + "parent", + "native_layer_name", + "size", + ) def __init__( self, @@ -86,17 +93,36 @@ class ObjectInformation(ReadOnlyMapping): native_layer_name: If this object references other objects (such as a pointer), what layer those objects live in size: The size that the whole structure consumes in bytes """ - super().__init__( - { - "layer_name": layer_name, - "offset": offset, - "member_name": member_name, - "parent": parent, - "native_layer_name": native_layer_name or layer_name, - "size": size, - } + self.layer_name = layer_name + self.offset = offset + self.member_name = member_name + self.parent = parent + self.native_layer_name = native_layer_name or layer_name + self.size = size + + def __getattr__(self, attr: str) -> Any: + """Returns the item as an attribute.""" + if attr in self.__slots__: + return getattr(self, attr) + raise AttributeError( + f"Object has no attribute: {self.__class__.__name__}.{attr}" ) + def __getitem__(self, name: str) -> Any: + """Returns the item requested.""" + return getattr(self, name) + + def __iter__(self): + """Returns an iterator of the dictionary items.""" + return self.__slots__.__iter__() + + def __len__(self) -> int: + """Returns the length of the internal dictionary.""" + return len(self.__slots__) + + def __eq__(self, other): + return dict(self) == dict(other) + class ObjectInterface(metaclass=abc.ABCMeta): """A base object required to be the ancestor of every object used in @@ -137,6 +163,7 @@ class ObjectInterface(metaclass=abc.ABCMeta): vol_info_dict = {"type_name": type_name, "offset": normalized_offset} self._vol.update(object_info) self._vol.update(vol_info_dict) + self._vol = collections.ChainMap({}, self._vol) self._context = context def __getattr__(self, attr: str) -> Any: @@ -321,6 +348,7 @@ class Template: super().__init__() self._vol = {"type_name": type_name} self._vol.update(arguments) + self._vol = collections.ChainMap({}, self._vol) @property def vol(self) -> ReadOnlyMapping: diff --git a/volatility3/framework/renderers/__init__.py b/volatility3/framework/renderers/__init__.py index cc3129e87..093edf8cc 100644 --- a/volatility3/framework/renderers/__init__.py +++ b/volatility3/framework/renderers/__init__.py @@ -61,7 +61,7 @@ class TreeNode(interfaces.renderers.TreeNode): self._treegrid = treegrid self._parent = parent self._path = path - validated_values = self._validate_values(values) + self._validate_values(values) self._values = treegrid.RowStructure(*values) # type: ignore def __repr__(self) -> str: @@ -73,12 +73,9 @@ class TreeNode(interfaces.renderers.TreeNode): def __len__(self) -> int: return len(self._treegrid.children(self)) - def _validate_values( - self, values: List[interfaces.renderers.BaseTypes] - ) -> List[interfaces.renderers.BaseTypes]: + def _validate_values(self, values: List[interfaces.renderers.BaseTypes]) -> None: """A function for raising exceptions if a given set of values is invalid according to the column properties.""" - new_values = () if not ( isinstance(values, collections.abc.Sequence) and len(values) == len(self._treegrid.columns) From 0ea5d795fdfbe3f562d6d04fde5a6192de29003b Mon Sep 17 00:00:00 2001 From: Mike Auty Date: Mon, 24 Mar 2025 22:36:12 +0000 Subject: [PATCH 24/68] Fix ruff issue --- volatility3/framework/interfaces/objects.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/volatility3/framework/interfaces/objects.py b/volatility3/framework/interfaces/objects.py index 995a5f29b..93c0ac16f 100644 --- a/volatility3/framework/interfaces/objects.py +++ b/volatility3/framework/interfaces/objects.py @@ -8,7 +8,7 @@ import collections import collections.abc import contextlib import logging -from typing import Any, Dict, List, Mapping, Optional +from typing import Any, List, Mapping, Optional from volatility3.framework import constants, interfaces From 62d1d818b3f751534baae6b2fbc0a063e43d183c Mon Sep 17 00:00:00 2001 From: Mike Auty Date: Mon, 24 Mar 2025 22:40:14 +0000 Subject: [PATCH 25/68] Restore use of ChainMap as well --- volatility3/framework/interfaces/objects.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/volatility3/framework/interfaces/objects.py b/volatility3/framework/interfaces/objects.py index 93c0ac16f..e1d36abb4 100644 --- a/volatility3/framework/interfaces/objects.py +++ b/volatility3/framework/interfaces/objects.py @@ -346,9 +346,8 @@ class Template: """Stores the keyword arguments for later object creation.""" # Allow the updating of template arguments whilst still in template form super().__init__() - self._vol = {"type_name": type_name} - self._vol.update(arguments) - self._vol = collections.ChainMap({}, self._vol) + vol = {"type_name": type_name}.update(arguments) + self._vol = collections.ChainMap({}, vol) @property def vol(self) -> ReadOnlyMapping: @@ -392,7 +391,7 @@ class Template: def clone(self) -> "Template": """Returns a copy of the original Template as constructed (without `update_vol` additions having been made)""" - clone = self.__class__(**self._vol) + clone = self.__class__(**self._vol.parents.new_child()) return clone def update_vol(self, **new_arguments) -> None: From 292ed6aeb46ff9bbbfbd0a8f73460bebf21f32b7 Mon Sep 17 00:00:00 2001 From: Mike Auty Date: Mon, 24 Mar 2025 22:41:52 +0000 Subject: [PATCH 26/68] Try to avoid variables changing types --- volatility3/framework/interfaces/objects.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/volatility3/framework/interfaces/objects.py b/volatility3/framework/interfaces/objects.py index e1d36abb4..d6ad47bec 100644 --- a/volatility3/framework/interfaces/objects.py +++ b/volatility3/framework/interfaces/objects.py @@ -159,11 +159,11 @@ class ObjectInterface(metaclass=abc.ABCMeta): mask = context.layers[object_info.layer_name].address_mask normalized_offset = object_info.offset & mask - self._vol = kwargs + vol = kwargs vol_info_dict = {"type_name": type_name, "offset": normalized_offset} - self._vol.update(object_info) - self._vol.update(vol_info_dict) - self._vol = collections.ChainMap({}, self._vol) + vol.update(object_info) + vol.update(vol_info_dict) + self._vol = collections.ChainMap({}, vol) self._context = context def __getattr__(self, attr: str) -> Any: From 200746cbe6264e8e84b9b4ade0f9116b0848c45d Mon Sep 17 00:00:00 2001 From: Mike Auty Date: Mon, 24 Mar 2025 22:46:36 +0000 Subject: [PATCH 27/68] Fix silly usage of update --- volatility3/framework/interfaces/objects.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/volatility3/framework/interfaces/objects.py b/volatility3/framework/interfaces/objects.py index d6ad47bec..7f4667b4e 100644 --- a/volatility3/framework/interfaces/objects.py +++ b/volatility3/framework/interfaces/objects.py @@ -346,7 +346,8 @@ class Template: """Stores the keyword arguments for later object creation.""" # Allow the updating of template arguments whilst still in template form super().__init__() - vol = {"type_name": type_name}.update(arguments) + vol = {"type_name": type_name} + vol.update(arguments) self._vol = collections.ChainMap({}, vol) @property From acfedd6d9cbbdca0a8058cec5cefc303455f591e Mon Sep 17 00:00:00 2001 From: Mike Auty Date: Mon, 24 Mar 2025 23:23:22 +0000 Subject: [PATCH 28/68] Sets slots to none has no effect on memory as long as __dict__ isn't instanciated --- volatility3/framework/interfaces/objects.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/volatility3/framework/interfaces/objects.py b/volatility3/framework/interfaces/objects.py index 7f4667b4e..e77a5893c 100644 --- a/volatility3/framework/interfaces/objects.py +++ b/volatility3/framework/interfaces/objects.py @@ -128,8 +128,6 @@ class ObjectInterface(metaclass=abc.ABCMeta): """A base object required to be the ancestor of every object used in volatility.""" - __slots__ = () - def __init__( self, context: "interfaces.context.ContextInterface", From a013170a2d2c22d48ae0f90354640b19117b5cd2 Mon Sep 17 00:00:00 2001 From: Mike Auty Date: Mon, 24 Mar 2025 23:58:45 +0000 Subject: [PATCH 29/68] Slotting has little effect, so don't change so much --- volatility3/framework/interfaces/objects.py | 54 +++++---------------- 1 file changed, 11 insertions(+), 43 deletions(-) diff --git a/volatility3/framework/interfaces/objects.py b/volatility3/framework/interfaces/objects.py index e77a5893c..b7ea616c7 100644 --- a/volatility3/framework/interfaces/objects.py +++ b/volatility3/framework/interfaces/objects.py @@ -8,7 +8,7 @@ import collections import collections.abc import contextlib import logging -from typing import Any, List, Mapping, Optional +from typing import Any, Dict, List, Mapping, Optional from volatility3.framework import constants, interfaces @@ -23,8 +23,6 @@ class ReadOnlyMapping(collections.abc.Mapping): modified, making an immutable mapping. """ - __slots__ = ("_dict",) - def __init__(self, dictionary: Mapping[str, Any]) -> None: self._dict = dictionary @@ -54,7 +52,7 @@ class ReadOnlyMapping(collections.abc.Mapping): return dict(self) == dict(other) -class ObjectInformation(collections.abc.Mapping): +class ObjectInformation(ReadOnlyMapping): """Contains common information useful/pertinent only to an individual object (like an instance) @@ -65,15 +63,6 @@ class ObjectInformation(collections.abc.Mapping): in a single place. These values are based on the :class:`ReadOnlyMapping` class, to prevent their modification. """ - __slots__ = ( - "layer_name", - "offset", - "member_name", - "parent", - "native_layer_name", - "size", - ) - def __init__( self, layer_name: str, @@ -93,36 +82,17 @@ class ObjectInformation(collections.abc.Mapping): native_layer_name: If this object references other objects (such as a pointer), what layer those objects live in size: The size that the whole structure consumes in bytes """ - self.layer_name = layer_name - self.offset = offset - self.member_name = member_name - self.parent = parent - self.native_layer_name = native_layer_name or layer_name - self.size = size - - def __getattr__(self, attr: str) -> Any: - """Returns the item as an attribute.""" - if attr in self.__slots__: - return getattr(self, attr) - raise AttributeError( - f"Object has no attribute: {self.__class__.__name__}.{attr}" + super().__init__( + { + "layer_name": layer_name, + "offset": offset, + "member_name": member_name, + "parent": parent, + "native_layer_name": native_layer_name or layer_name, + "size": size, + } ) - def __getitem__(self, name: str) -> Any: - """Returns the item requested.""" - return getattr(self, name) - - def __iter__(self): - """Returns an iterator of the dictionary items.""" - return self.__slots__.__iter__() - - def __len__(self) -> int: - """Returns the length of the internal dictionary.""" - return len(self.__slots__) - - def __eq__(self, other): - return dict(self) == dict(other) - class ObjectInterface(metaclass=abc.ABCMeta): """A base object required to be the ancestor of every object used in @@ -338,8 +308,6 @@ class Template: constructed at resolution time and then cached. """ - __slots__ = "_vol" - def __init__(self, type_name: str, **arguments) -> None: """Stores the keyword arguments for later object creation.""" # Allow the updating of template arguments whilst still in template form From de11e87f28661f8b30d7d2c38ea8480461a0536c Mon Sep 17 00:00:00 2001 From: Mike Auty Date: Tue, 25 Mar 2025 00:08:07 +0000 Subject: [PATCH 30/68] Fix ruff error (again) --- volatility3/framework/interfaces/objects.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/volatility3/framework/interfaces/objects.py b/volatility3/framework/interfaces/objects.py index b7ea616c7..1bca7a045 100644 --- a/volatility3/framework/interfaces/objects.py +++ b/volatility3/framework/interfaces/objects.py @@ -8,7 +8,7 @@ import collections import collections.abc import contextlib import logging -from typing import Any, Dict, List, Mapping, Optional +from typing import Any, List, Mapping, Optional from volatility3.framework import constants, interfaces From ee0ad0b4af9c094976390f65376d623345dab393 Mon Sep 17 00:00:00 2001 From: Mike Auty Date: Fri, 28 Mar 2025 01:46:14 +0000 Subject: [PATCH 31/68] Update the various MINOR version bumps to the current version --- API_CHANGES.md | 96 ++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 94 insertions(+), 2 deletions(-) diff --git a/API_CHANGES.md b/API_CHANGES.md index 61d8781fb..83d59a202 100644 --- a/API_CHANGES.md +++ b/API_CHANGES.md @@ -4,6 +4,100 @@ API Changes When an addition to the existing API is made, the minor version is bumped. When an API feature or function is removed or changed, the major version is bumped. +2.25.0 +====== +Pointer class now supports `get_raw_value()`. +`KTIMER` no longer supports `get_raw_dpc()`. + +2.24.0 +====== +Support `encoding` parameter for `objects.utility.array_to_string` + +2.23.0 +====== +Add support for windows GUI classes and OS distinguishers. +Add a symbol_table_name for `ExecutiveObject.get_object_header()`/ + +2.22.0 +====== +Linux net constants added. +Network objects moved to separate versionable module. + +2.21.0 +====== +`uuid` method added to `linux.extensions`. + +2.20.0 +====== +NM_TYPES_DESC constants added to linux. +`latch_tree_root` and `kernel_symbol` added to linux extensions. +Linux `module` class additions: +* `get_module_address_boundaries` +* `section_typetab` +Linux `task_struct` class additions: +* `get_address_space_layer` +* `state` +Linux `bpf_prog` class additions: +* `bpf_jit_binary_hdr_address` + +2.19.0 +====== +Introduction of `Modules` versionable linux extension module. +Deprecation of some `LinuxUtilities` functions relating to modules. + +2.18.0 +====== +Addition of `scatterlist` linux extension. + +2.17.0 +====== +The addition of a `types` member to `SymbolInterface` + +2.16.0 +====== +Addition of TAINT_FLAG constants, `TaintFlag` dataclass +Addition of linux `tainting` versionable module + +2.15.0 +====== +Addition of `convert_fourcc_code` to `LinuxUtilities` class + +2.14.0 +====== +No significant changes (part of the 2.16.0 PR which took time in development) + +2.13.0 +====== +Linux `task` objectr extension addition of `getppid` + +2.12.0 +====== +Changes to the Intel layer to support `PROT_NONE` pages. + +2.11.0 +====== +Addition of `get_type` method to windows `CM_KEY_NODE` registry structure + +2.10.0 +====== +No significant API changes (CLI changes to the JSONL text renderer) + +2.9.0 +===== +No significant API changes (change to call `linux.LinuxUtilities.get_module_from_volobj_type` to get the kernel) + +2.8.0 +===== +Addition of the `BinOrAbsent`, `HexOrAbsent`, `HexBytesOrAbsent` and `MultiTypeDataOrAbsent` data type renderers + +2.7.0 +===== +Addition of `is_valid`, `get_create_time` and `get_exit_time` to ETHREAD structure + +2.6.0 +===== +No significant changes (again, the version got bump twice in the PR straight to 2.7.0) + 2.5.0 ===== Add in support for specifying a type override for object_from_symbol @@ -50,5 +144,3 @@ an absolute offset. This can be done with `Module.get_absolute_symbol_address` * Added context.modules * Added ModuleRequirement * Added get\_symbols\_by\_absolute\_location - - From f844f80d2a0bc9c39e92def94a312bcc0aa7b81f Mon Sep 17 00:00:00 2001 From: ikelos Date: Fri, 28 Mar 2025 14:29:35 +0000 Subject: [PATCH 32/68] Update API_CHANGES.md Fix typo highlighted by @eve-mem --- API_CHANGES.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/API_CHANGES.md b/API_CHANGES.md index 83d59a202..a4d8d9b13 100644 --- a/API_CHANGES.md +++ b/API_CHANGES.md @@ -68,7 +68,7 @@ No significant changes (part of the 2.16.0 PR which took time in development) 2.13.0 ====== -Linux `task` objectr extension addition of `getppid` +Linux `task` object extension addition of `getppid` 2.12.0 ====== From 2ad1536b4e154f601f28247e15640ccc4bc0ad84 Mon Sep 17 00:00:00 2001 From: David McDonald Date: Mon, 24 Mar 2025 11:59:38 -0500 Subject: [PATCH 33/68] Testing: Verify `VersionRequirement`s This adds a script and GitHub action to the `test` directory that dynamically imports all modules in `volatility3`, searches for usages of `VersionableInterface` objects within classes that inherit from `ConfigurableInterface` but don't enumerate the used component as a requirement in `get_requirements()`, and returns -1 if any violations are found. Fixes --- .github/workflows/check-requirements.yml | 25 ++ pyproject.toml | 2 + test/check_configurable_requirements.py | 300 +++++++++++++++++++++++ 3 files changed, 327 insertions(+) create mode 100644 .github/workflows/check-requirements.yml create mode 100644 test/check_configurable_requirements.py diff --git a/.github/workflows/check-requirements.yml b/.github/workflows/check-requirements.yml new file mode 100644 index 000000000..9892d7b94 --- /dev/null +++ b/.github/workflows/check-requirements.yml @@ -0,0 +1,25 @@ +name: Check Volatility3 Version Requirements +on: [push, pull_request] +jobs: + + build: + runs-on: ubuntu-22.04 + strategy: + matrix: + python-version: ["3.8"] + steps: + - uses: actions/checkout@v4 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install .[test] + + - name: Testing... + run: | + # Verify completeness of ConfigurableInterface requirements + python ./test/check_configurable_requirements.py diff --git a/pyproject.toml b/pyproject.toml index abd2e79f7..8bc3693a2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -46,6 +46,8 @@ test = [ "volatility3[dev]", "pytest>=8.3.3,<9", "yara-x>=0.10.0,<1", + "tree-sitter==0.21.3", + "tree-sitter-python==0.21.0", ] docs = [ diff --git a/test/check_configurable_requirements.py b/test/check_configurable_requirements.py new file mode 100644 index 000000000..e21df18ac --- /dev/null +++ b/test/check_configurable_requirements.py @@ -0,0 +1,300 @@ +import importlib +import inspect +import pkgutil +import sys +import traceback +import types +from textwrap import dedent +from typing import Dict, Iterator, List, NamedTuple, Optional, Tuple, Type + +from tree_sitter import Language, Node, Parser +from tree_sitter_python import language as python_language + +from volatility3.framework import configuration, interfaces + + +class UnrequiredVersionableUsage(NamedTuple): + versionable_item_class: str + """ + The name of the VersionableInterface class + """ + + consuming_class: str + """ + The name of the class that is using the imported VersionableInterface class + """ + + methodname: Optional[str] + """ + The name of the invoked method or attribute, if one is used or referenced + """ + + node: Node + """ + The tree-sitter node encapsulating the used module component. + """ + + def __str__(self) -> str: + return ( + f"Found usage of {self.versionable_item_class} " + f"in class {self.consuming_class} that is not declared " + f"in {self.consuming_class}'s `get_requirements()` classmethod" + ) + + +class RequirementValidator: + language = Language(python_language(), "python") + + def __init__(self, plugin_module: types.ModuleType) -> None: + if plugin_module.__file__ is None: + raise ValueError("Attempting to validate a module without a file") + + self._module = plugin_module + + # See which classes in *this* module are configurable (can have requirements declared) + self._configurable_classes = get_configurable_classes(plugin_module) + + # Get a mapping of class names to configurable classes that they declare in their requirements + self._versioned_item_mapping = get_versioned_item_mapping( + self._configurable_classes + ) + + # Get a mapping of module name -> versionable classes within the namespace of each module + self._imported_mod_classes = get_versionable_import_mapping( + get_imported_modules(plugin_module) + ) + + with open(plugin_module.__file__, "rb") as f: + source = f.read() + + self._parser = Parser() + self._parser.set_language(self.language) + self._tree = self._parser.parse(source) + + def enumerate_unrequired_usages( + self, + clazz: Type[interfaces.configuration.ConfigurableInterface], + class_node: Node, + ): + + # This query is designed to look for three different identifier usages: + # simple identifiers: PsList + # module attrs: pslist.PsList + # method calls: pslist.PsList.list_processes + obj_query = self.language.query( + dedent( + """ + [ + (identifier) + (attribute + object: (identifier) + attribute: (identifier)) + (attribute + object: (attribute + object: (identifier) + attribute: (identifier)) + ) + ] @ident + """ + ) + ) + + containing_name = class_node.child_by_field_name("name").text.decode("utf-8") + + valid_types = self._versioned_item_mapping[containing_name] + for _, match in obj_query.matches(class_node): + if "ident" not in match: + continue + + # Get the raw text of the match. This could be something like + # - PsList + # - pslist.PsList + # - pslist.PsList.list_processes + ident_text = match["ident"].text.decode("utf-8") + + # split the attributes + components = ident_text.split(".") + try: + # See if the first attribute is in the module namespace. + item = vars(self._module)[components[0]] + except KeyError: + # If it's not, it's likely a variable in a smaller scope and we + # can ignore it. + continue + + # If it's in the module namespace and is a module... + if isinstance(item, types.ModuleType): + try: + # We try getting attributes from it until we + # find one that is a versionable class + + # Ideally, we shouldn't have to look further than + # two levels + item = getattr(item, components[1]) + if not is_versionable(item): + item = getattr(item, components[2]) + if not is_versionable(item): + continue + + except (IndexError, AttributeError): + # we ran out of attributes to check + continue + + elif is_versionable(item): + # The versionable thing was at the top level. This + # goes against our preferred style, but is possible. + pass + else: + # This isn't something we care about. + continue + + if ( + item in valid_types + or item is clazz + or inspect.isabstract(item) + or item + is interfaces.configuration.VersionableInterface # Avoid checking the interface itself + ): + continue + + yield UnrequiredVersionableUsage( + item, + containing_name, + components[1] if len(components) > 1 else None, + match["ident"], + ) + + def find_class_nodes( + self, + ) -> Iterator[Tuple[Type[interfaces.configuration.ConfigurableInterface], Node]]: + """ + Yields an iterator of (classname, node) tuples, where the node is the subtree containing + the entire class definition. + """ + class_query = self.language.query("(class_definition) @classdef") + + matches = class_query.captures(self._tree.root_node) + for node, _ in matches: + classname = node.child_by_field_name("name").text.decode("utf-8") + if classname not in self._configurable_classes: + continue + + yield self._configurable_classes[classname], node + + +def is_versionable(var): + try: + return issubclass(var, interfaces.configuration.VersionableInterface) + except TypeError: + return False + + +def is_configurable(var): + try: + return issubclass(var, interfaces.configuration.ConfigurableInterface) + except TypeError: + return False + + +def get_imported_modules( + plugin_module: types.ModuleType, +) -> List[Tuple[str, types.ModuleType]]: + return [ + (name, var) + for name, var in vars(plugin_module).items() + if isinstance(var, types.ModuleType) + ] + + +def get_configurable_classes( + plugin_module: types.ModuleType, +) -> Dict[str, Type[interfaces.configuration.ConfigurableInterface]]: + return { + name: clazz + for name, clazz in vars(plugin_module).items() + if is_configurable(clazz) + } + + +def get_versioned_item_mapping( + configurable_classes: Dict[ + str, Type[interfaces.configuration.ConfigurableInterface] + ] +) -> Dict[str, List[Type[interfaces.configuration.VersionableInterface]]]: + return { + name: [ + req._component + for req in clazz.get_requirements() + if isinstance(req, configuration.requirements.VersionRequirement) + ] + for name, clazz in configurable_classes.items() + } + + +def get_versionable_import_mapping( + imported_modules: List[Tuple[str, types.ModuleType]] +) -> Dict[str, List[str]]: + return { + modname: [name for name, var in vars(module).items() if is_versionable(var)] + for modname, module in imported_modules + } + + +def report_missing_requirements() -> Iterator[Tuple[str, UnrequiredVersionableUsage]]: + vol3 = importlib.import_module("volatility3") + + for _, module_name, _ in pkgutil.walk_packages( + vol3.__path__, vol3.__name__ + ".", onerror=lambda _: None + ): + try: + # import the module that we want to check + modname = module_name.replace( + "volatility3.framework.plugins", "volatility3.plugins" + ) + plugin_module = importlib.import_module(modname) + + except ImportError: + continue + except Exception: + continue + + if plugin_module.__file__ is None: + continue + + try: + # construct a validator for the module + try: + validator = RequirementValidator(plugin_module) + except Exception: + traceback.print_stack() + continue + for clazz, node in validator.find_class_nodes(): + for item in validator.enumerate_unrequired_usages(clazz, node): + yield module_name, item + except Exception as exc: + traceback.print_exc() + print( + f"Failed to create validator for source code from {plugin_module.__file__}: {exc}" + ) + sys.exit(1) + + +def perform_review(): + found = 0 + for mod, usage in report_missing_requirements(): + found += 1 + print( + f"Violation in module {mod} (line {usage.node.start_point[0]}): {str(usage)}" + ) + + if found: + print( + f"Found {found} uses of versionable components not declared in get_requirements()" + ) + sys.exit(1) + + print("All configurable classes passed validation!") + + +if __name__ == "__main__": + perform_review() From 0f73686364392dbba36a4a20fff2a13f8df04c31 Mon Sep 17 00:00:00 2001 From: David McDonald Date: Thu, 27 Mar 2025 11:07:58 -0500 Subject: [PATCH 34/68] Framework: Fix remaining missing requirements This adds all of the missing requirements discovered via the new code analysis script. --- volatility3/cli/volshell/generic.py | 7 ++++++ volatility3/cli/volshell/linux.py | 5 ++++ volatility3/cli/volshell/mac.py | 5 ++++ volatility3/cli/volshell/windows.py | 5 ++++ volatility3/framework/automagic/pdbscan.py | 23 +++++++++++++++---- .../framework/automagic/symbol_finder.py | 7 +++++- volatility3/framework/layers/qemu.py | 11 +++++++++ .../framework/layers/scanners/__init__.py | 5 ++++ volatility3/framework/plugins/banners.py | 7 +++++- volatility3/framework/plugins/linux/bash.py | 10 ++++++++ .../framework/plugins/linux/check_modules.py | 5 ++++ .../framework/plugins/linux/hidden_modules.py | 5 ++++ volatility3/framework/plugins/linux/psscan.py | 5 ++++ .../framework/plugins/linux/vmaregexscan.py | 5 ++++ volatility3/framework/plugins/mac/bash.py | 10 ++++++++ .../framework/plugins/mac/list_files.py | 5 ++++ volatility3/framework/plugins/regexscan.py | 5 ++++ volatility3/framework/plugins/vmscan.py | 7 ++++++ .../framework/plugins/windows/cmdscan.py | 5 ++++ .../framework/plugins/windows/consoles.py | 5 ++++ .../framework/plugins/windows/mbrscan.py | 5 ++++ .../framework/plugins/windows/poolscanner.py | 7 ++++++ .../plugins/windows/skeleton_key_check.py | 5 ++++ .../framework/plugins/windows/svclist.py | 5 ++++ .../framework/plugins/windows/svcscan.py | 5 ++++ .../framework/plugins/windows/vadregexscan.py | 5 ++++ .../framework/plugins/windows/verinfo.py | 5 ++++ volatility3/framework/plugins/yarascan.py | 7 +++++- 28 files changed, 179 insertions(+), 7 deletions(-) diff --git a/volatility3/cli/volshell/generic.py b/volatility3/cli/volshell/generic.py index 3a5d514fe..1b3ae59d1 100644 --- a/volatility3/cli/volshell/generic.py +++ b/volatility3/cli/volshell/generic.py @@ -38,6 +38,8 @@ class Volshell(interfaces.plugins.PluginInterface): _required_framework_version = (2, 0, 0) + _version = (1, 0, 0) + DEFAULT_NUM_DISPLAY_BYTES = 128 def __init__(self, *args, **kwargs): @@ -61,6 +63,11 @@ class Volshell(interfaces.plugins.PluginInterface): default=None, optional=True, ), + requirements.VersionRequirement( + name="regex_scanner", + component=scanners.RegExScanner, + version=(1, 0, 0), + ), requirements.BooleanRequirement( name="script-only", description="Exit volshell after the script specified in --script completes", diff --git a/volatility3/cli/volshell/linux.py b/volatility3/cli/volshell/linux.py index 27c630614..761b64084 100644 --- a/volatility3/cli/volshell/linux.py +++ b/volatility3/cli/volshell/linux.py @@ -36,6 +36,11 @@ class Volshell(generic.Volshell): requirements.IntRequirement( name="pid", description="Process ID", optional=True ), + requirements.VersionRequirement( + name="generic_volshell", + component=generic.Volshell, + version=(1, 0, 0), + ), ] def change_task(self, pid=None): diff --git a/volatility3/cli/volshell/mac.py b/volatility3/cli/volshell/mac.py index 393eff20b..fcb45e124 100644 --- a/volatility3/cli/volshell/mac.py +++ b/volatility3/cli/volshell/mac.py @@ -25,6 +25,11 @@ class Volshell(generic.Volshell): requirements.IntRequirement( name="pid", description="Process ID", optional=True ), + requirements.VersionRequirement( + name="generic_volshell", + component=generic.Volshell, + version=(1, 0, 0), + ), ] def change_task(self, pid=None): diff --git a/volatility3/cli/volshell/windows.py b/volatility3/cli/volshell/windows.py index ce5995648..a8c7af5b3 100644 --- a/volatility3/cli/volshell/windows.py +++ b/volatility3/cli/volshell/windows.py @@ -23,6 +23,11 @@ class Volshell(generic.Volshell): requirements.IntRequirement( name="pid", description="Process ID", optional=True ), + requirements.VersionRequirement( + name="generic_volshell", + component=generic.Volshell, + version=(1, 0, 0), + ), ] def change_process(self, pid=None): diff --git a/volatility3/framework/automagic/pdbscan.py b/volatility3/framework/automagic/pdbscan.py index dd2ad0683..55b9b81e1 100644 --- a/volatility3/framework/automagic/pdbscan.py +++ b/volatility3/framework/automagic/pdbscan.py @@ -17,7 +17,7 @@ from volatility3.framework import constants, exceptions, interfaces, layers from volatility3.framework.configuration import requirements from volatility3.framework.layers import intel, scanners from volatility3.framework.symbols import native -from volatility3.framework.symbols.windows.pdbutil import PDBUtility +from volatility3.framework.symbols.windows import pdbutil if __name__ == "__main__": import sys @@ -50,6 +50,21 @@ class KernelPDBScanner(interfaces.automagic.AutomagicInterface): max_pdb_size = 0x400000 exclusion_list = ["linux", "mac"] + @classmethod + def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]: + return [ + requirements.VersionRequirement( + name="pdb_utility", + component=pdbutil.PDBUtility, + version=(1, 0, 1), + ), + requirements.VersionRequirement( + name="bytes_scanner", + component=scanners.BytesScanner, + version=(1, 0, 0), + ), + ] + def find_virtual_layers_from_req( self, context: interfaces.context.ContextInterface, @@ -120,7 +135,7 @@ class KernelPDBScanner(interfaces.automagic.AutomagicInterface): ): raise TypeError("PDB name or GUID not a string value") - PDBUtility.load_windows_symbol_table( + pdbutil.PDBUtility.load_windows_symbol_table( context=context, guid=kernel["GUID"], age=kernel["age"], @@ -259,7 +274,7 @@ class KernelPDBScanner(interfaces.automagic.AutomagicInterface): bytes(name + ".pdb", "utf-8") for name in constants.windows.KERNEL_MODULE_NAMES ] - kernels = PDBUtility.pdbname_scan( + kernels = pdbutil.PDBUtility.pdbname_scan( ctx=context, layer_name=layer_to_scan, start=start_scan_address, @@ -362,7 +377,7 @@ class KernelPDBScanner(interfaces.automagic.AutomagicInterface): with contextlib.suppress(exceptions.InvalidAddressException): if vlayer.read(address, 0x2) == b"MZ": res = list( - PDBUtility.pdbname_scan( + pdbutil.PDBUtility.pdbname_scan( ctx=context, layer_name=vlayer.name, page_size=vlayer.page_size, diff --git a/volatility3/framework/automagic/symbol_finder.py b/volatility3/framework/automagic/symbol_finder.py index 1d30f3f51..d7c6a22c1 100644 --- a/volatility3/framework/automagic/symbol_finder.py +++ b/volatility3/framework/automagic/symbol_finder.py @@ -40,7 +40,12 @@ class SymbolFinder(interfaces.automagic.AutomagicInterface): name="SQLiteCache", component=symbol_cache.SqliteCache, version=(1, 0, 0), - ) + ), + requirements.VersionRequirement( + name="multi_string_scanner", + component=scanners.MultiStringScanner, + version=(1, 0, 0), + ), ] @property diff --git a/volatility3/framework/layers/qemu.py b/volatility3/framework/layers/qemu.py index a8127e954..eb44de347 100644 --- a/volatility3/framework/layers/qemu.py +++ b/volatility3/framework/layers/qemu.py @@ -9,6 +9,7 @@ import struct from typing import Any, Dict, List, Optional, Set, Tuple from volatility3.framework import constants, exceptions, interfaces +from volatility3.framework.configuration import requirements from volatility3.framework.layers import scanners, segmented from volatility3.framework.symbols import intermed @@ -99,6 +100,16 @@ class QemuSuspendLayer(segmented.NonLinearlySegmentedLayer): context=context, config_path=config_path, name=name, metadata=metadata ) + @classmethod + def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]: + return [ + requirements.VersionRequirement( + name="regex_scanner", + component=scanners.RegExScanner, + version=(1, 0, 0), + ), + ] + @classmethod def _check_header( cls, base_layer: interfaces.layers.DataLayerInterface, name: str = "" diff --git a/volatility3/framework/layers/scanners/__init__.py b/volatility3/framework/layers/scanners/__init__.py index be9f1c39a..f07849f42 100644 --- a/volatility3/framework/layers/scanners/__init__.py +++ b/volatility3/framework/layers/scanners/__init__.py @@ -11,6 +11,8 @@ from volatility3.framework.layers.scanners import multiregexp as multiregexp class BytesScanner(layers.ScannerInterface): thread_safe = True + _version = (1, 0, 0) + _required_framework_version = (2, 0, 0) def __init__(self, needle: bytes) -> None: @@ -38,6 +40,8 @@ class RegExScanner(layers.ScannerInterface): thread_safe = True + _version = (1, 0, 0) + _required_framework_version = (2, 0, 0) def __init__(self, pattern: bytes, flags: int = re.DOTALL) -> None: @@ -57,6 +61,7 @@ class RegExScanner(layers.ScannerInterface): class MultiStringScanner(layers.ScannerInterface): thread_safe = True + _version = (1, 0, 0) _required_framework_version = (2, 0, 0) def __init__(self, patterns: List[bytes]) -> None: diff --git a/volatility3/framework/plugins/banners.py b/volatility3/framework/plugins/banners.py index b3c2fd3a5..d4e6e2aa8 100644 --- a/volatility3/framework/plugins/banners.py +++ b/volatility3/framework/plugins/banners.py @@ -22,7 +22,12 @@ class Banners(interfaces.plugins.PluginInterface): return [ requirements.TranslationLayerRequirement( name="primary", description="Memory layer to scan" - ) + ), + requirements.VersionRequirement( + name="regex_scanner", + component=scanners.RegExScanner, + version=(1, 0, 0), + ), ] def _generator(self): diff --git a/volatility3/framework/plugins/linux/bash.py b/volatility3/framework/plugins/linux/bash.py index 2a63ac329..382b66194 100644 --- a/volatility3/framework/plugins/linux/bash.py +++ b/volatility3/framework/plugins/linux/bash.py @@ -40,6 +40,16 @@ class Bash(plugins.PluginInterface, timeliner.TimeLinerInterface): component=timeliner.TimeLinerInterface, version=(1, 0, 0), ), + requirements.VersionRequirement( + name="multi_string_scanner", + component=scanners.MultiStringScanner, + version=(1, 0, 0), + ), + requirements.VersionRequirement( + name="bytes_scanner", + component=scanners.BytesScanner, + version=(1, 0, 0), + ), requirements.ListRequirement( name="pid", element_type=int, diff --git a/volatility3/framework/plugins/linux/check_modules.py b/volatility3/framework/plugins/linux/check_modules.py index ec6f0b73d..7805bbd8a 100644 --- a/volatility3/framework/plugins/linux/check_modules.py +++ b/volatility3/framework/plugins/linux/check_modules.py @@ -46,6 +46,11 @@ class Check_modules(plugins.PluginInterface): @classmethod def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]: return [ + requirements.VersionRequirement( + name="modules", + component=linux_utilities_modules.Modules, + version=(3, 0, 1), + ), requirements.VersionRequirement( name="linux_utilities_modules_module_display_plugin", component=linux_utilities_modules.ModuleDisplayPlugin, diff --git a/volatility3/framework/plugins/linux/hidden_modules.py b/volatility3/framework/plugins/linux/hidden_modules.py index 136aafdd9..dcd602c5d 100644 --- a/volatility3/framework/plugins/linux/hidden_modules.py +++ b/volatility3/framework/plugins/linux/hidden_modules.py @@ -93,6 +93,11 @@ class Hidden_modules(plugins.PluginInterface): component=linux_utilities_modules.ModuleDisplayPlugin, version=(1, 0, 0), ), + requirements.VersionRequirement( + name="linux_utilities_modules", + component=linux_utilities_modules.Modules, + version=(3, 0, 1), + ), ] + linux_utilities_modules.ModuleDisplayPlugin.get_requirements() @staticmethod diff --git a/volatility3/framework/plugins/linux/psscan.py b/volatility3/framework/plugins/linux/psscan.py index 0813cebed..0013bc1d8 100644 --- a/volatility3/framework/plugins/linux/psscan.py +++ b/volatility3/framework/plugins/linux/psscan.py @@ -41,6 +41,11 @@ class PsScan(interfaces.plugins.PluginInterface): requirements.VersionRequirement( name="pslist", component=pslist.PsList, version=(4, 0, 0) ), + requirements.VersionRequirement( + name="multi_string_scanner", + component=scanners.MultiStringScanner, + version=(1, 0, 0), + ), ] def _generator(self): diff --git a/volatility3/framework/plugins/linux/vmaregexscan.py b/volatility3/framework/plugins/linux/vmaregexscan.py index 4c8ef5b8f..37a1a5940 100644 --- a/volatility3/framework/plugins/linux/vmaregexscan.py +++ b/volatility3/framework/plugins/linux/vmaregexscan.py @@ -46,6 +46,11 @@ class VmaRegExScan(plugins.PluginInterface): requirements.StringRequirement( name="pattern", description="RegEx pattern", optional=False ), + requirements.VersionRequirement( + name="regex_scanner", + component=scanners.RegExScanner, + version=(1, 0, 0), + ), requirements.IntRequirement( name="maxsize", description="Maximum size in bytes for displayed context", diff --git a/volatility3/framework/plugins/mac/bash.py b/volatility3/framework/plugins/mac/bash.py index 4cbade1cf..5ad6facd0 100644 --- a/volatility3/framework/plugins/mac/bash.py +++ b/volatility3/framework/plugins/mac/bash.py @@ -38,6 +38,16 @@ class Bash(plugins.PluginInterface, timeliner.TimeLinerInterface): component=timeliner.TimeLinerInterface, version=(1, 0, 0), ), + requirements.VersionRequirement( + name="multi_string_scanner", + component=scanners.MultiStringScanner, + version=(1, 0, 0), + ), + requirements.VersionRequirement( + name="bytes_scanner", + component=scanners.BytesScanner, + version=(1, 0, 0), + ), requirements.ListRequirement( name="pid", description="Filter on specific process IDs", diff --git a/volatility3/framework/plugins/mac/list_files.py b/volatility3/framework/plugins/mac/list_files.py index bf3dcfce6..423e2e0da 100644 --- a/volatility3/framework/plugins/mac/list_files.py +++ b/volatility3/framework/plugins/mac/list_files.py @@ -31,6 +31,11 @@ class List_Files(plugins.PluginInterface): requirements.VersionRequirement( name="mount", component=mount.Mount, version=(2, 0, 0) ), + requirements.VersionRequirement( + name="mac_utilities", + component=mac.MacUtilities, + version=(1, 3, 0), + ), ] @classmethod diff --git a/volatility3/framework/plugins/regexscan.py b/volatility3/framework/plugins/regexscan.py index c526b1697..343753e92 100644 --- a/volatility3/framework/plugins/regexscan.py +++ b/volatility3/framework/plugins/regexscan.py @@ -39,6 +39,11 @@ class RegExScan(plugins.PluginInterface): default=cls.MAXSIZE_DEFAULT, optional=True, ), + requirements.VersionRequirement( + name="regex_scanner", + component=scanners.RegExScanner, + version=(1, 0, 0), + ), ] def _generator(self, regex_pattern): diff --git a/volatility3/framework/plugins/vmscan.py b/volatility3/framework/plugins/vmscan.py index 64377d7d8..5322456b5 100644 --- a/volatility3/framework/plugins/vmscan.py +++ b/volatility3/framework/plugins/vmscan.py @@ -26,6 +26,8 @@ class VMCSTest(enum.IntFlag): class PageStartScanner(interfaces.layers.ScannerInterface): + _version = (1, 0, 0) + def __init__(self, signatures: List[bytes], page_size: int = 0x1000): super().__init__() if not len(signatures): @@ -69,6 +71,11 @@ class Vmscan(plugins.PluginInterface): requirements.TranslationLayerRequirement( name="primary", description="Physical base memory layer" ), + requirements.VersionRequirement( + name="page_start_scanner", + component=PageStartScanner, + version=(1, 0, 0), + ), requirements.IntRequirement( name="log-threshold", description="Number of criteria failed to log to debug output", diff --git a/volatility3/framework/plugins/windows/cmdscan.py b/volatility3/framework/plugins/windows/cmdscan.py index 8c477b57d..676050b65 100644 --- a/volatility3/framework/plugins/windows/cmdscan.py +++ b/volatility3/framework/plugins/windows/cmdscan.py @@ -41,6 +41,11 @@ class CmdScan(interfaces.plugins.PluginInterface): requirements.VersionRequirement( name="consoles", component=consoles.Consoles, version=(3, 0, 0) ), + requirements.VersionRequirement( + name="bytes_scanner", + component=scanners.BytesScanner, + version=(1, 0, 0), + ), requirements.BooleanRequirement( name="no_registry", description="Don't search the registry for possible values of CommandHistorySize", diff --git a/volatility3/framework/plugins/windows/consoles.py b/volatility3/framework/plugins/windows/consoles.py index 6cfc6d588..efc03ad1b 100644 --- a/volatility3/framework/plugins/windows/consoles.py +++ b/volatility3/framework/plugins/windows/consoles.py @@ -54,6 +54,11 @@ class Consoles(interfaces.plugins.PluginInterface): requirements.VersionRequirement( name="hivelist", component=hivelist.HiveList, version=(2, 0, 0) ), + requirements.VersionRequirement( + name="bytes_scanner", + component=scanners.BytesScanner, + version=(1, 0, 0), + ), requirements.BooleanRequirement( name="no_registry", description="Don't search the registry for possible values of CommandHistorySize and HistoryBufferMax", diff --git a/volatility3/framework/plugins/windows/mbrscan.py b/volatility3/framework/plugins/windows/mbrscan.py index 4d5198181..541aae60d 100644 --- a/volatility3/framework/plugins/windows/mbrscan.py +++ b/volatility3/framework/plugins/windows/mbrscan.py @@ -37,6 +37,11 @@ class MBRScan(interfaces.plugins.PluginInterface): default=False, optional=True, ), + requirements.VersionRequirement( + name="multi_string_scanner", + component=scanners.MultiStringScanner, + version=(1, 0, 0), + ), ] @classmethod diff --git a/volatility3/framework/plugins/windows/poolscanner.py b/volatility3/framework/plugins/windows/poolscanner.py index 975ed2326..7929b70e4 100644 --- a/volatility3/framework/plugins/windows/poolscanner.py +++ b/volatility3/framework/plugins/windows/poolscanner.py @@ -55,6 +55,8 @@ class PoolConstraint: class PoolHeaderScanner(interfaces.layers.ScannerInterface): + _version = (1, 0, 0) + def __init__( self, module: interfaces.context.ModuleInterface, @@ -142,6 +144,11 @@ class PoolScanner(plugins.PluginInterface): requirements.VersionRequirement( name="handles", component=handles.Handles, version=(3, 0, 0) ), + requirements.VersionRequirement( + name="pool_header_scanner", + component=PoolHeaderScanner, + version=(1, 0, 0), + ), ] def _generator(self): diff --git a/volatility3/framework/plugins/windows/skeleton_key_check.py b/volatility3/framework/plugins/windows/skeleton_key_check.py index 4831362fd..6071a2a39 100644 --- a/volatility3/framework/plugins/windows/skeleton_key_check.py +++ b/volatility3/framework/plugins/windows/skeleton_key_check.py @@ -63,6 +63,11 @@ class Skeleton_Key_Check(interfaces.plugins.PluginInterface): requirements.VersionRequirement( name="pe_symbols", component=pe_symbols.PESymbols, version=(3, 0, 0) ), + requirements.VersionRequirement( + name="bytes_scanner", + component=scanners.BytesScanner, + version=(1, 0, 0), + ), ] def _check_for_skeleton_key_vad( diff --git a/volatility3/framework/plugins/windows/svclist.py b/volatility3/framework/plugins/windows/svclist.py index 24ac2278f..963b7fc71 100644 --- a/volatility3/framework/plugins/windows/svclist.py +++ b/volatility3/framework/plugins/windows/svclist.py @@ -42,6 +42,11 @@ class SvcList(svcscan.SvcScan): description="Windows kernel", architectures=["Intel32", "Intel64"], ), + requirements.VersionRequirement( + name="bytes_scanner", + component=scanners.BytesScanner, + version=(1, 0, 0), + ), ] @classmethod diff --git a/volatility3/framework/plugins/windows/svcscan.py b/volatility3/framework/plugins/windows/svcscan.py index 94ce02897..5f0e4761e 100644 --- a/volatility3/framework/plugins/windows/svcscan.py +++ b/volatility3/framework/plugins/windows/svcscan.py @@ -56,6 +56,11 @@ class SvcScan(interfaces.plugins.PluginInterface): requirements.VersionRequirement( name="hivelist", component=hivelist.HiveList, version=(2, 0, 0) ), + requirements.VersionRequirement( + name="bytes_scanner", + component=scanners.BytesScanner, + version=(1, 0, 0), + ), ] @classmethod diff --git a/volatility3/framework/plugins/windows/vadregexscan.py b/volatility3/framework/plugins/windows/vadregexscan.py index 9b666cbcb..5ead4e453 100644 --- a/volatility3/framework/plugins/windows/vadregexscan.py +++ b/volatility3/framework/plugins/windows/vadregexscan.py @@ -41,6 +41,11 @@ class VadRegExScan(plugins.PluginInterface): element_type=int, optional=True, ), + requirements.VersionRequirement( + name="regex_scanner", + component=scanners.RegExScanner, + version=(1, 0, 0), + ), requirements.StringRequirement( name="pattern", description="RegEx pattern", optional=False ), diff --git a/volatility3/framework/plugins/windows/verinfo.py b/volatility3/framework/plugins/windows/verinfo.py index b5eba7ec6..d2722418b 100644 --- a/volatility3/framework/plugins/windows/verinfo.py +++ b/volatility3/framework/plugins/windows/verinfo.py @@ -48,6 +48,11 @@ class VerInfo(interfaces.plugins.PluginInterface): requirements.VersionRequirement( name="modules", component=modules.Modules, version=(3, 0, 0) ), + requirements.VersionRequirement( + name="page_start_scanner", + component=scanners.BytesScanner, + version=(1, 0, 0), + ), requirements.BooleanRequirement( name="extensive", description="Search physical layer for version information", diff --git a/volatility3/framework/plugins/yarascan.py b/volatility3/framework/plugins/yarascan.py index 38c8b6085..bb86ab6f1 100644 --- a/volatility3/framework/plugins/yarascan.py +++ b/volatility3/framework/plugins/yarascan.py @@ -118,7 +118,12 @@ class YaraScan(plugins.PluginInterface): name="primary", description="Memory layer for the kernel", architectures=["Intel32", "Intel64"], - ) + ), + requirements.VersionRequirement( + name="yarascanner", + component=YaraScanner, + version=(2, 1, 1), + ), ] @classmethod From 68116556a8fbe01d63e7d8866d8b9330902d0bed Mon Sep 17 00:00:00 2001 From: David McDonald Date: Thu, 27 Mar 2025 12:54:08 -0500 Subject: [PATCH 35/68] Add calls to super().get_requirements() on inherited classes --- volatility3/cli/volshell/linux.py | 2 +- volatility3/cli/volshell/mac.py | 2 +- volatility3/cli/volshell/windows.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/volatility3/cli/volshell/linux.py b/volatility3/cli/volshell/linux.py index 761b64084..8b9e236b4 100644 --- a/volatility3/cli/volshell/linux.py +++ b/volatility3/cli/volshell/linux.py @@ -41,7 +41,7 @@ class Volshell(generic.Volshell): component=generic.Volshell, version=(1, 0, 0), ), - ] + ] + super().get_requirements() def change_task(self, pid=None): """Change the current process and layer, based on a process ID""" diff --git a/volatility3/cli/volshell/mac.py b/volatility3/cli/volshell/mac.py index fcb45e124..190c7b9f7 100644 --- a/volatility3/cli/volshell/mac.py +++ b/volatility3/cli/volshell/mac.py @@ -30,7 +30,7 @@ class Volshell(generic.Volshell): component=generic.Volshell, version=(1, 0, 0), ), - ] + ] + super().get_requirements() def change_task(self, pid=None): """Change the current process and layer, based on a process ID""" diff --git a/volatility3/cli/volshell/windows.py b/volatility3/cli/volshell/windows.py index a8c7af5b3..e7e37ed61 100644 --- a/volatility3/cli/volshell/windows.py +++ b/volatility3/cli/volshell/windows.py @@ -28,7 +28,7 @@ class Volshell(generic.Volshell): component=generic.Volshell, version=(1, 0, 0), ), - ] + ] + super().get_requirements() def change_process(self, pid=None): """Change the current process and layer, based on a process ID""" From 9f024cf0f485cae6c03a4fb980687c1930f97a51 Mon Sep 17 00:00:00 2001 From: David McDonald Date: Thu, 27 Mar 2025 17:30:17 -0500 Subject: [PATCH 36/68] Refactor: use builtin ast lib instead of treesitter Instead of using the tree-sitter third party library, this uses Python's `ast` module to parse the source code and traverse the tree with a visitor pattern. This is preferred because it's native to the language itself, and Python developers are more likely to be familiar with it. The traversal also handles nested scopes better than the prior implementation. For example, classes that are declared inside of other classes can now be looked up even though they don't exist at the top level of the module namespace, since any time a class definition is entered, that class is pushed to the top of a stack that can be examined when visiting inner classes. This also adds lots of log messages at different levels, plus a command line argument for specifying verbosity, which should help with debugging down the line. --- pyproject.toml | 2 - test/check_configurable_requirements.py | 464 +++++++++++++----------- 2 files changed, 253 insertions(+), 213 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 8bc3693a2..abd2e79f7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -46,8 +46,6 @@ test = [ "volatility3[dev]", "pytest>=8.3.3,<9", "yara-x>=0.10.0,<1", - "tree-sitter==0.21.3", - "tree-sitter-python==0.21.0", ] docs = [ diff --git a/test/check_configurable_requirements.py b/test/check_configurable_requirements.py index e21df18ac..d532d98b3 100644 --- a/test/check_configurable_requirements.py +++ b/test/check_configurable_requirements.py @@ -1,16 +1,57 @@ +import argparse +import ast import importlib import inspect +import logging import pkgutil import sys -import traceback import types -from textwrap import dedent -from typing import Dict, Iterator, List, NamedTuple, Optional, Tuple, Type - -from tree_sitter import Language, Node, Parser -from tree_sitter_python import language as python_language +from typing import Any, Iterator, NamedTuple, Optional, Tuple, Type, Union from volatility3.framework import configuration, interfaces +from volatility3.framework.deprecation import PluginRenameClass + +logging.basicConfig(format="%(levelname)s: %(message)s") +logger = logging.getLogger(__name__) + + +class NodeVisitor: + def visit(self, node): + """Visit a node.""" + method = "visit_" + node.__class__.__name__ + visitor = getattr(self, method, self.generic_visit) + self.enter(node) + result = visitor(node) + self.leave(node) + return result + + def enter(self, node): + """Called when entering a node.""" + method = "enter_" + node.__class__.__name__ + visitor = getattr(self, method, self.generic_enter) + return visitor(node) + + def leave(self, node): + """Called when leaving a node.""" + method = "leave_" + node.__class__.__name__ + visitor = getattr(self, method, self.generic_leave) + return visitor(node) + + def generic_visit(self, node): + """Called if no explicit visitor function exists for a node.""" + for _, value in ast.iter_fields(node): + if isinstance(value, list): + for item in value: + if isinstance(item, ast.AST): + self.visit(item) + elif isinstance(value, ast.AST): + self.visit(value) + + def generic_enter(self, node): + """Default enter behavior.""" + + def generic_leave(self, node): + """Default leave behavior.""" class UnrequiredVersionableUsage(NamedTuple): @@ -24,12 +65,7 @@ class UnrequiredVersionableUsage(NamedTuple): The name of the class that is using the imported VersionableInterface class """ - methodname: Optional[str] - """ - The name of the invoked method or attribute, if one is used or referenced - """ - - node: Node + node: Union[ast.Name, ast.Attribute] """ The tree-sitter node encapsulating the used module component. """ @@ -42,149 +78,13 @@ class UnrequiredVersionableUsage(NamedTuple): ) -class RequirementValidator: - language = Language(python_language(), "python") - - def __init__(self, plugin_module: types.ModuleType) -> None: - if plugin_module.__file__ is None: - raise ValueError("Attempting to validate a module without a file") - - self._module = plugin_module - - # See which classes in *this* module are configurable (can have requirements declared) - self._configurable_classes = get_configurable_classes(plugin_module) - - # Get a mapping of class names to configurable classes that they declare in their requirements - self._versioned_item_mapping = get_versioned_item_mapping( - self._configurable_classes - ) - - # Get a mapping of module name -> versionable classes within the namespace of each module - self._imported_mod_classes = get_versionable_import_mapping( - get_imported_modules(plugin_module) - ) - - with open(plugin_module.__file__, "rb") as f: - source = f.read() - - self._parser = Parser() - self._parser.set_language(self.language) - self._tree = self._parser.parse(source) - - def enumerate_unrequired_usages( - self, - clazz: Type[interfaces.configuration.ConfigurableInterface], - class_node: Node, - ): - - # This query is designed to look for three different identifier usages: - # simple identifiers: PsList - # module attrs: pslist.PsList - # method calls: pslist.PsList.list_processes - obj_query = self.language.query( - dedent( - """ - [ - (identifier) - (attribute - object: (identifier) - attribute: (identifier)) - (attribute - object: (attribute - object: (identifier) - attribute: (identifier)) - ) - ] @ident - """ - ) - ) - - containing_name = class_node.child_by_field_name("name").text.decode("utf-8") - - valid_types = self._versioned_item_mapping[containing_name] - for _, match in obj_query.matches(class_node): - if "ident" not in match: - continue - - # Get the raw text of the match. This could be something like - # - PsList - # - pslist.PsList - # - pslist.PsList.list_processes - ident_text = match["ident"].text.decode("utf-8") - - # split the attributes - components = ident_text.split(".") - try: - # See if the first attribute is in the module namespace. - item = vars(self._module)[components[0]] - except KeyError: - # If it's not, it's likely a variable in a smaller scope and we - # can ignore it. - continue - - # If it's in the module namespace and is a module... - if isinstance(item, types.ModuleType): - try: - # We try getting attributes from it until we - # find one that is a versionable class - - # Ideally, we shouldn't have to look further than - # two levels - item = getattr(item, components[1]) - if not is_versionable(item): - item = getattr(item, components[2]) - if not is_versionable(item): - continue - - except (IndexError, AttributeError): - # we ran out of attributes to check - continue - - elif is_versionable(item): - # The versionable thing was at the top level. This - # goes against our preferred style, but is possible. - pass - else: - # This isn't something we care about. - continue - - if ( - item in valid_types - or item is clazz - or inspect.isabstract(item) - or item - is interfaces.configuration.VersionableInterface # Avoid checking the interface itself - ): - continue - - yield UnrequiredVersionableUsage( - item, - containing_name, - components[1] if len(components) > 1 else None, - match["ident"], - ) - - def find_class_nodes( - self, - ) -> Iterator[Tuple[Type[interfaces.configuration.ConfigurableInterface], Node]]: - """ - Yields an iterator of (classname, node) tuples, where the node is the subtree containing - the entire class definition. - """ - class_query = self.language.query("(class_definition) @classdef") - - matches = class_query.captures(self._tree.root_node) - for node, _ in matches: - classname = node.child_by_field_name("name").text.decode("utf-8") - if classname not in self._configurable_classes: - continue - - yield self._configurable_classes[classname], node - - def is_versionable(var): try: - return issubclass(var, interfaces.configuration.VersionableInterface) + return ( + issubclass(var, interfaces.configuration.VersionableInterface) + and var is not interfaces.configuration.VersionableInterface + and not inspect.isabstract(var) + ) except TypeError: return False @@ -196,48 +96,162 @@ def is_configurable(var): return False -def get_imported_modules( - plugin_module: types.ModuleType, -) -> List[Tuple[str, types.ModuleType]]: - return [ - (name, var) - for name, var in vars(plugin_module).items() - if isinstance(var, types.ModuleType) - ] +class ModuleVisitor(NodeVisitor): + def __init__(self, module: types.ModuleType) -> None: + self._module = module + self._scopes = [] + self._violations = [] + + @property + def violations(self): + return self._violations + + def enter_ClassDef(self, node: ast.ClassDef) -> Any: + logger.debug("Entering class %s", node.name) + clazz = None + try: + clazz = vars(self._module)[str(node.name)] + except KeyError: + logger.debug( + "Failed to get %s from module scope: (%s)", + node.name, + self._module.__name__, + ) + if self._scopes: + try: + logger.debug( + "Attempting to get class %s from scope of %s", + node.name, + self._scopes[-1].__name__, + ) + clazz = getattr(self._scopes[-1], node.name) + except AttributeError: + logger.debug( + "Class not found in scope of %s", self._scopes[-1].__name__ + ) + if clazz: + self._scopes.append(clazz) + + if clazz and is_configurable(clazz): + logger.info("Checking configurable class %s", clazz.__name__) + visitor = ConfigurableClassVisitor(self._module, clazz) + visitor.visit(node) + self._violations += visitor.violations + + self.generic_visit(node) + + def leave_ClassDef(self, node: ast.ClassDef): + logger.debug("Leaving class %s", node.name) + try: + scoped_class = next( + scope for scope in self._scopes if scope.__name__ == node.name + ) + self._scopes.remove(scoped_class) + except StopIteration: + logger.debug("%s not found in scope list", node.name) -def get_configurable_classes( - plugin_module: types.ModuleType, -) -> Dict[str, Type[interfaces.configuration.ConfigurableInterface]]: - return { - name: clazz - for name, clazz in vars(plugin_module).items() - if is_configurable(clazz) - } +class ConfigurableClassVisitor(NodeVisitor): + def __init__( + self, + module: types.ModuleType, + clazz: Optional[Type[interfaces.configuration.ConfigurableInterface]], + ) -> None: + self._module = module + self._current_object = None + self._clazz = clazz + self._seen = set() + self._violations = [] + @property + def versioned_classes(self): + return ( + [ + req._component + for req in self._clazz.get_requirements() + if isinstance(req, configuration.requirements.VersionRequirement) + ] + if self._clazz is not None + else [] + ) -def get_versioned_item_mapping( - configurable_classes: Dict[ - str, Type[interfaces.configuration.ConfigurableInterface] - ] -) -> Dict[str, List[Type[interfaces.configuration.VersionableInterface]]]: - return { - name: [ - req._component - for req in clazz.get_requirements() - if isinstance(req, configuration.requirements.VersionRequirement) - ] - for name, clazz in configurable_classes.items() - } + def check_item(self, item: Type, node: Union[ast.Name, ast.Attribute]): + if ( + is_versionable(item) + and self._clazz is not None + and item not in self.versioned_classes + and item is not self._clazz + and not issubclass(self._clazz, PluginRenameClass) + ): + logger.info( + "Found versionable item %s, checking against %s", + str(item), + str(self.versioned_classes), + ) + result = UnrequiredVersionableUsage( + item.__name__, self._clazz.__name__, node + ) + self._violations.append(result) + @property + def violations(self): + return self._violations -def get_versionable_import_mapping( - imported_modules: List[Tuple[str, types.ModuleType]] -) -> Dict[str, List[str]]: - return { - modname: [name for name, var in vars(module).items() if is_versionable(var)] - for modname, module in imported_modules - } + def visit_Name(self, node: ast.Name): + try: + logger.debug( + "Checking module %s for name %s", self._module.__name__, node.id + ) + item = vars(self._module)[str(node.id)] + logger.debug("Found %s in %s namespace", node.id, self._module.__name__) + except KeyError: + return + + self.check_item(item, node) + + def visit_Attribute( + self, node: ast.Attribute + ) -> Optional[UnrequiredVersionableUsage]: + if self._clazz is None: + self.generic_visit(node) + return + + if (node.lineno, node.col_offset) in self._seen: + return + + self._seen.add((node.lineno, node.col_offset)) + + stack = [] + root = node + while True: + stack.append(node.attr) + if isinstance(node.value, ast.Attribute): + node = node.value + elif isinstance(node.value, ast.Name): + stack.append(node.value.id) + break + else: + break + + current = None + logger.debug("Checking %s", ".".join(stack[::-1])) + for item in stack[::-1]: + try: + current = ( + vars(self._module)[item] + if current is None + else getattr(current, item) + ) + except (KeyError, AttributeError) as exc: + logger.debug( + "Failed to get attribute %s (%s)%s", + item, + exc.__class__.__name__, + (" on" + str(current)) if current is not None else "", + ) + break + + self.check_item(current, root) def report_missing_requirements() -> Iterator[Tuple[str, UnrequiredVersionableUsage]]: @@ -246,46 +260,60 @@ def report_missing_requirements() -> Iterator[Tuple[str, UnrequiredVersionableUs for _, module_name, _ in pkgutil.walk_packages( vol3.__path__, vol3.__name__ + ".", onerror=lambda _: None ): + modname = module_name.replace( + "volatility3.framework.plugins", "volatility3.plugins" + ) try: # import the module that we want to check - modname = module_name.replace( - "volatility3.framework.plugins", "volatility3.plugins" - ) plugin_module = importlib.import_module(modname) - except ImportError: + except ImportError as exc: + logger.warning("Failed to import %s: %s", modname, str(exc)) continue - except Exception: + except Exception as exc: + logger.warning( + "An unexpected exception occurred while importing %s: %s", + modname, + str(exc), + ) continue + logger.info("Checking module %s", plugin_module.__name__) if plugin_module.__file__ is None: + logger.warning("Plugin module %s has no source file", modname) continue try: - # construct a validator for the module - try: - validator = RequirementValidator(plugin_module) - except Exception: - traceback.print_stack() - continue - for clazz, node in validator.find_class_nodes(): - for item in validator.enumerate_unrequired_usages(clazz, node): - yield module_name, item - except Exception as exc: - traceback.print_exc() - print( - f"Failed to create validator for source code from {plugin_module.__file__}: {exc}" + with open(plugin_module.__file__, "rb") as f: + source = f.read() + except OSError: + logger.warning( + "Failed to read file contents for %s", plugin_module.__file__ + ) + continue + + try: + module_ast_root = ast.parse(source) + except (SyntaxError, ValueError) as exc: + logger.warning( + "Failed to parse source for %s: %s", plugin_module.__file__, str(exc) + ) + raise + + mod_visitor = ModuleVisitor(plugin_module) + mod_visitor.visit(module_ast_root) + + if mod_visitor.violations: + yield from ( + (plugin_module.__name__, res) for res in iter(mod_visitor.violations) ) - sys.exit(1) def perform_review(): found = 0 for mod, usage in report_missing_requirements(): found += 1 - print( - f"Violation in module {mod} (line {usage.node.start_point[0]}): {str(usage)}" - ) + print(f"Violation in module {mod} (line {usage.node.lineno}): {str(usage)}") if found: print( @@ -296,5 +324,19 @@ def perform_review(): print("All configurable classes passed validation!") +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser() + parser.add_argument("-v", "--verbose", action="count", dest="verbosity", default=0) + return parser.parse_args() + + if __name__ == "__main__": + args = parse_args() + if args.verbosity == 0: + logger.setLevel(logging.WARNING) + elif args.verbosity == 1: + logger.setLevel(logging.INFO) + elif args.verbosity > 1: + logger.setLevel(logging.DEBUG) + perform_review() From 03c647790206267ecf4c3d4921b2a0164ea4dcd1 Mon Sep 17 00:00:00 2001 From: David McDonald Date: Fri, 28 Mar 2025 13:04:24 -0500 Subject: [PATCH 37/68] Volshell: Attempt to resolve requirement conflicts This change sets the `script`, `script-only`, and `primary` requirements to only apply to the `generic.Volshell` class. `regex-scanner` is okay to be shared between the base and inherited classes, but `script` and `script-only` have to be generic-only in order to avoid conflicts when populating the argparse parser. `primary` must be generic-only in order to avoid ending up unsatisfied when superclass requirements require a module, suppressing construction of the `primary` layer. --- volatility3/cli/volshell/generic.py | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/volatility3/cli/volshell/generic.py b/volatility3/cli/volshell/generic.py index 1b3ae59d1..39e4fc963 100644 --- a/volatility3/cli/volshell/generic.py +++ b/volatility3/cli/volshell/generic.py @@ -54,20 +54,24 @@ class Volshell(interfaces.plugins.PluginInterface): @classmethod def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]: - reqs: List[interfaces.configuration.RequirementInterface] = [] + reqs: List[interfaces.configuration.RequirementInterface] = [ + requirements.VersionRequirement( + name="regex_scanner", + component=scanners.RegExScanner, + version=(1, 0, 0), + ), + ] if cls == Volshell: - reqs = [ + reqs += [ + requirements.TranslationLayerRequirement( + name="primary", description="Memory layer for the kernel" + ), requirements.URIRequirement( name="script", description="File to load and execute at start", default=None, optional=True, ), - requirements.VersionRequirement( - name="regex_scanner", - component=scanners.RegExScanner, - version=(1, 0, 0), - ), requirements.BooleanRequirement( name="script-only", description="Exit volshell after the script specified in --script completes", @@ -75,11 +79,8 @@ class Volshell(interfaces.plugins.PluginInterface): optional=True, ), ] - return reqs + [ - requirements.TranslationLayerRequirement( - name="primary", description="Memory layer for the kernel" - ), - ] + + return reqs def run( self, additional_locals: Dict[str, Any] = {} From 27e59263a6825886cb5d8f1524f2e8d48955b57f Mon Sep 17 00:00:00 2001 From: David McDonald Date: Fri, 28 Mar 2025 13:17:38 -0500 Subject: [PATCH 38/68] Docstring: explain version-checking script This documents the general behavior and expectations of the version-checking CI script. --- test/check_configurable_requirements.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/test/check_configurable_requirements.py b/test/check_configurable_requirements.py index d532d98b3..f856c80b7 100644 --- a/test/check_configurable_requirements.py +++ b/test/check_configurable_requirements.py @@ -1,3 +1,19 @@ +""" +This script performs syntax analysis on the volatility3 source tree through a combination of AST analysis and import-time introspection of classes. + +The current checks it implements are: + 1. Ensure that classes derived from `ConfigurableInterface` properly + declare all `VersionableInterface` classes that they make use of in their + `get_requirements()` classmethod. + + :WARNING: a notable exception to this are classes defined within factory + functions. Because these classes are not created until the factory function + is called, they therefore do no exist at import time and cannot be checked + by this script. It is important to keep in mind during code review that + this is a best-effort check and does not make guarantees about the + completeness of declared requirements. +""" + import argparse import ast import importlib From d0a1daf82c1a118b1b33958e4cd6dec2243df248 Mon Sep 17 00:00:00 2001 From: David McDonald Date: Fri, 28 Mar 2025 13:24:51 -0500 Subject: [PATCH 39/68] ModuleExtract: Add missing requirement --- volatility3/framework/plugins/linux/module_extract.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/volatility3/framework/plugins/linux/module_extract.py b/volatility3/framework/plugins/linux/module_extract.py index d7c875523..97824aca0 100644 --- a/volatility3/framework/plugins/linux/module_extract.py +++ b/volatility3/framework/plugins/linux/module_extract.py @@ -36,6 +36,11 @@ class ModuleExtract(interfaces.plugins.PluginInterface): description="Base virtual address to reconstruct an ELF file", optional=False, ), + requirements.VersionRequirement( + name="linux_utilities_module_extract", + version=(1, 0, 0), + component=linux_utilities_module_extract.ModuleExtract, + ), ] def _generator(self): From e21eb57b9009dec3046ac1c9d5e2e4cf93b42b6c Mon Sep 17 00:00:00 2001 From: eve Date: Fri, 28 Mar 2025 18:40:51 +0000 Subject: [PATCH 40/68] Volshell: handle case where paged out member would cause backtrace for dt output. Thanks to @atcuno for the code! --- volatility3/cli/volshell/generic.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/volatility3/cli/volshell/generic.py b/volatility3/cli/volshell/generic.py index 3a5d514fe..c153d281d 100644 --- a/volatility3/cli/volshell/generic.py +++ b/volatility3/cli/volshell/generic.py @@ -446,8 +446,14 @@ class Volshell(interfaces.plugins.PluginInterface): len_offset = len(hex(relative_offset)) len_member = len(member) len_typename = len(member_type.vol.type_name) + if isinstance(volobject, interfaces.objects.ObjectInterface): # We're an instance, so also display the data + try: + value = self._display_value(getattr(volobject, member)) + except exceptions.InvalidAddressException: + value = self._display_value(renderers.NotAvailableValue()) + print( " " * (longest_offset - len_offset), hex(relative_offset), @@ -458,7 +464,7 @@ class Volshell(interfaces.plugins.PluginInterface): member_type.vol.type_name, " " * (longest_typename - len_typename), " ", - self._display_value(getattr(volobject, member)), + value, ) else: print( @@ -473,7 +479,9 @@ class Volshell(interfaces.plugins.PluginInterface): @classmethod def _display_value(cls, value: Any) -> str: - if isinstance(value, objects.PrimitiveObject): + if isinstance(value, interfaces.renderers.BaseAbsentValue): + return "N/A" + elif isinstance(value, objects.PrimitiveObject): return repr(value) elif isinstance(value, objects.Array): return repr([cls._display_value(val) for val in value]) From 23f2157931df51ff79d02f7d34e39691451e240a Mon Sep 17 00:00:00 2001 From: eve Date: Fri, 28 Mar 2025 18:50:10 +0000 Subject: [PATCH 41/68] Volshell: update display_type to handle struct members that are also python functions, e.g. write(). Thanks to @atcuno for the suggestion --- volatility3/cli/volshell/generic.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/volatility3/cli/volshell/generic.py b/volatility3/cli/volshell/generic.py index c153d281d..a487fa3cd 100644 --- a/volatility3/cli/volshell/generic.py +++ b/volatility3/cli/volshell/generic.py @@ -450,7 +450,7 @@ class Volshell(interfaces.plugins.PluginInterface): if isinstance(volobject, interfaces.objects.ObjectInterface): # We're an instance, so also display the data try: - value = self._display_value(getattr(volobject, member)) + value = self._display_value(volobject.member(member)) except exceptions.InvalidAddressException: value = self._display_value(renderers.NotAvailableValue()) From 196556eab3ed0abbffd20bcec169d2f131535426 Mon Sep 17 00:00:00 2001 From: David McDonald Date: Fri, 28 Mar 2025 15:18:22 -0500 Subject: [PATCH 42/68] Test: Allow for other types of coding style violations --- test/check_configurable_requirements.py | 51 +++++++++++++++---------- 1 file changed, 31 insertions(+), 20 deletions(-) diff --git a/test/check_configurable_requirements.py b/test/check_configurable_requirements.py index f856c80b7..89e06e751 100644 --- a/test/check_configurable_requirements.py +++ b/test/check_configurable_requirements.py @@ -14,6 +14,7 @@ The current checks it implements are: completeness of declared requirements. """ +import abc import argparse import ast import importlib @@ -22,7 +23,7 @@ import logging import pkgutil import sys import types -from typing import Any, Iterator, NamedTuple, Optional, Tuple, Type, Union +from typing import Any, Iterator, List, Optional, Tuple, Type, Union from volatility3.framework import configuration, interfaces from volatility3.framework.deprecation import PluginRenameClass @@ -70,27 +71,37 @@ class NodeVisitor: """Default leave behavior.""" -class UnrequiredVersionableUsage(NamedTuple): - versionable_item_class: str - """ - The name of the VersionableInterface class - """ +class CodeViolation(metaclass=abc.ABCMeta): + def __init__(self, module: types.ModuleType, node: ast.AST) -> None: + self.module = module + self.node = node - consuming_class: str - """ - The name of the class that is using the imported VersionableInterface class - """ + def __str__(self): + return f"Code violation in module {self.module.__name__}: line {self.node.lineno}, col {self.node.col_offset}" - node: Union[ast.Name, ast.Attribute] - """ - The tree-sitter node encapsulating the used module component. - """ + +class UnrequiredVersionableUsage(CodeViolation): + + def __init__( + self, + module: types.ModuleType, + node: ast.AST, + consuming_class: str, + versionable_item_class: str, + ) -> None: + super().__init__(module, node) + self.consuming_class = consuming_class + self.versionable_item_class = versionable_item_class def __str__(self) -> str: return ( - f"Found usage of {self.versionable_item_class} " - f"in class {self.consuming_class} that is not declared " - f"in {self.consuming_class}'s `get_requirements()` classmethod" + super().__str__() + + ": " + + ( + f"Found usage of {self.versionable_item_class} " + f"in class {self.consuming_class} that is not declared " + f"in {self.consuming_class}'s `get_requirements()` classmethod" + ) ) @@ -177,7 +188,7 @@ class ConfigurableClassVisitor(NodeVisitor): self._current_object = None self._clazz = clazz self._seen = set() - self._violations = [] + self._violations: List[CodeViolation] = [] @property def versioned_classes(self): @@ -205,7 +216,7 @@ class ConfigurableClassVisitor(NodeVisitor): str(self.versioned_classes), ) result = UnrequiredVersionableUsage( - item.__name__, self._clazz.__name__, node + self._module, node, self._clazz.__name__, item.__name__ ) self._violations.append(result) @@ -333,7 +344,7 @@ def perform_review(): if found: print( - f"Found {found} uses of versionable components not declared in get_requirements()" + f"Found {found} coding standards violations" ) sys.exit(1) From 46e3b8ffdb4e9c4b536e2a6fc8217f2be3d77c4c Mon Sep 17 00:00:00 2001 From: David McDonald Date: Fri, 28 Mar 2025 15:21:54 -0500 Subject: [PATCH 43/68] Check for 'hidden' attribute when determining classes to validate --- test/check_configurable_requirements.py | 1 + 1 file changed, 1 insertion(+) diff --git a/test/check_configurable_requirements.py b/test/check_configurable_requirements.py index 89e06e751..ce65aca48 100644 --- a/test/check_configurable_requirements.py +++ b/test/check_configurable_requirements.py @@ -111,6 +111,7 @@ def is_versionable(var): issubclass(var, interfaces.configuration.VersionableInterface) and var is not interfaces.configuration.VersionableInterface and not inspect.isabstract(var) + and not (hasattr(var, "hidden") and getattr(var, "hidden") is True) ) except TypeError: return False From d7695ab9cfb507b3bb3e791f00bcd082f5de3afc Mon Sep 17 00:00:00 2001 From: David McDonald Date: Fri, 28 Mar 2025 15:36:54 -0500 Subject: [PATCH 44/68] Simplify error message output --- test/check_configurable_requirements.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/check_configurable_requirements.py b/test/check_configurable_requirements.py index ce65aca48..b643facb5 100644 --- a/test/check_configurable_requirements.py +++ b/test/check_configurable_requirements.py @@ -341,7 +341,7 @@ def perform_review(): found = 0 for mod, usage in report_missing_requirements(): found += 1 - print(f"Violation in module {mod} (line {usage.node.lineno}): {str(usage)}") + print(str(usage)) if found: print( From 6452fc18bd6cb614e8eaa747bc2d5be36a224e52 Mon Sep 17 00:00:00 2001 From: David McDonald Date: Fri, 28 Mar 2025 15:39:09 -0500 Subject: [PATCH 45/68] Tone down language severity in messages --- test/check_configurable_requirements.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/check_configurable_requirements.py b/test/check_configurable_requirements.py index b643facb5..864c3e89e 100644 --- a/test/check_configurable_requirements.py +++ b/test/check_configurable_requirements.py @@ -77,7 +77,7 @@ class CodeViolation(metaclass=abc.ABCMeta): self.node = node def __str__(self): - return f"Code violation in module {self.module.__name__}: line {self.node.lineno}, col {self.node.col_offset}" + return f"Issue in module {self.module.__name__}: line {self.node.lineno}, col {self.node.col_offset}" class UnrequiredVersionableUsage(CodeViolation): @@ -345,7 +345,7 @@ def perform_review(): if found: print( - f"Found {found} coding standards violations" + f"Found {found} issues" ) sys.exit(1) From 07f7a2e2be24e6e82c232228f4899c052fbb741f Mon Sep 17 00:00:00 2001 From: ikelos Date: Sat, 29 Mar 2025 12:48:47 +0000 Subject: [PATCH 46/68] Revert "Feature/use less memory" --- volatility3/framework/interfaces/objects.py | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/volatility3/framework/interfaces/objects.py b/volatility3/framework/interfaces/objects.py index 1bca7a045..62c31481b 100644 --- a/volatility3/framework/interfaces/objects.py +++ b/volatility3/framework/interfaces/objects.py @@ -8,7 +8,7 @@ import collections import collections.abc import contextlib import logging -from typing import Any, List, Mapping, Optional +from typing import Any, Dict, List, Mapping, Optional from volatility3.framework import constants, interfaces @@ -127,11 +127,8 @@ class ObjectInterface(metaclass=abc.ABCMeta): mask = context.layers[object_info.layer_name].address_mask normalized_offset = object_info.offset & mask - vol = kwargs vol_info_dict = {"type_name": type_name, "offset": normalized_offset} - vol.update(object_info) - vol.update(vol_info_dict) - self._vol = collections.ChainMap({}, vol) + self._vol = collections.ChainMap({}, vol_info_dict, object_info, kwargs) self._context = context def __getattr__(self, attr: str) -> Any: @@ -312,9 +309,10 @@ class Template: """Stores the keyword arguments for later object creation.""" # Allow the updating of template arguments whilst still in template form super().__init__() - vol = {"type_name": type_name} - vol.update(arguments) - self._vol = collections.ChainMap({}, vol) + empty_dict: Dict[str, Any] = {} + self._vol = collections.ChainMap( + empty_dict, arguments, {"type_name": type_name} + ) @property def vol(self) -> ReadOnlyMapping: From b73e4f0d2bc093d55879fac7387f30afa44920fe Mon Sep 17 00:00:00 2001 From: Mike Auty Date: Sat, 29 Mar 2025 14:32:14 +0000 Subject: [PATCH 47/68] Don't completely remove the chainmap, but change one dict to a namedmapping --- volatility3/framework/contexts/__init__.py | 2 +- volatility3/framework/interfaces/objects.py | 47 +++++++-------------- volatility3/framework/objects/__init__.py | 5 ++- 3 files changed, 20 insertions(+), 34 deletions(-) diff --git a/volatility3/framework/contexts/__init__.py b/volatility3/framework/contexts/__init__.py index e7c423a10..a000fce90 100644 --- a/volatility3/framework/contexts/__init__.py +++ b/volatility3/framework/contexts/__init__.py @@ -130,7 +130,7 @@ class Context(interfaces.context.ContextInterface): object_info=interfaces.objects.ObjectInformation( layer_name=layer_name, offset=offset, - native_layer_name=native_layer_name, + native_layer_name=native_layer_name or layer_name, size=object_template.size, ), ) diff --git a/volatility3/framework/interfaces/objects.py b/volatility3/framework/interfaces/objects.py index 62c31481b..2d8024465 100644 --- a/volatility3/framework/interfaces/objects.py +++ b/volatility3/framework/interfaces/objects.py @@ -8,7 +8,7 @@ import collections import collections.abc import contextlib import logging -from typing import Any, Dict, List, Mapping, Optional +from typing import Any, Dict, List, Mapping, NamedTuple, Optional from volatility3.framework import constants, interfaces @@ -52,7 +52,7 @@ class ReadOnlyMapping(collections.abc.Mapping): return dict(self) == dict(other) -class ObjectInformation(ReadOnlyMapping): +class ObjectInformation(NamedTuple): """Contains common information useful/pertinent only to an individual object (like an instance) @@ -63,35 +63,20 @@ class ObjectInformation(ReadOnlyMapping): in a single place. These values are based on the :class:`ReadOnlyMapping` class, to prevent their modification. """ - def __init__( - self, - layer_name: str, - offset: int, - member_name: Optional[str] = None, - parent: Optional["ObjectInterface"] = None, - native_layer_name: Optional[str] = None, - size: Optional[int] = None, - ): - """Constructs a container for basic information about an object. + layer_name: str + offset: int + native_layer_name: str + member_name: Optional[str] = None + parent: Optional["ObjectInterface"] = None + size: Optional[int] = None - Args: - layer_name: Layer from which the data for the object will be read - offset: Offset within the layer at which the data for the object will be read - member_name: If the object was accessed as a member of a parent object, this was the name used to access it - parent: If the object was accessed as a member of a parent object, this is the parent object - native_layer_name: If this object references other objects (such as a pointer), what layer those objects live in - size: The size that the whole structure consumes in bytes - """ - super().__init__( - { - "layer_name": layer_name, - "offset": offset, - "member_name": member_name, - "parent": parent, - "native_layer_name": native_layer_name or layer_name, - "size": size, - } - ) + def __getitem__(self, key): + if key in self._fields: + return getattr(self, key) + raise KeyError(f"NamedTuple does not have a key {key}") + + def __contains__(self, key): + return key in self._fields class ObjectInterface(metaclass=abc.ABCMeta): @@ -183,7 +168,7 @@ class ObjectInterface(metaclass=abc.ABCMeta): offset=self.vol.offset, member_name=self.vol.member_name, parent=self.vol.parent, - native_layer_name=self.vol.native_layer_name, + native_layer_name=self.vol.native_layer_name or self.vol.layer_name, size=object_template.size, ) return object_template(context=self._context, object_info=object_info) diff --git a/volatility3/framework/objects/__init__.py b/volatility3/framework/objects/__init__.py index b863e103b..08d6cb31e 100644 --- a/volatility3/framework/objects/__init__.py +++ b/volatility3/framework/objects/__init__.py @@ -458,6 +458,7 @@ class Pointer(Integer): offset=offset, parent=self, size=self.vol.subtype.size, + native_layer_name=layer_name, ), ) return self._cache[layer_name] @@ -811,7 +812,7 @@ class Array(interfaces.objects.ObjectInterface, collections.abc.Sequence): layer_name=self.vol.layer_name, offset=mask & (self.vol.offset + (self.vol.subtype.size * index)), parent=self, - native_layer_name=self.vol.native_layer_name, + native_layer_name=self.vol.native_layer_name or self.vol.layer_name, size=self.vol.subtype.size, ) result += [self.vol.subtype(context=self._context, object_info=object_info)] @@ -978,7 +979,7 @@ class AggregateType(interfaces.objects.ObjectInterface): offset=mask & (self.vol.offset + relative_offset), member_name=attr, parent=self, - native_layer_name=self.vol.native_layer_name, + native_layer_name=self.vol.native_layer_name or self.vol.layer_name, size=template.size, ) member = template(context=self._context, object_info=object_info) From e62cee391a2af40d0c7aaa03cc6d7555c87bf149 Mon Sep 17 00:00:00 2001 From: David McDonald Date: Fri, 28 Mar 2025 16:52:43 -0500 Subject: [PATCH 48/68] Testing: Adds validation of vol3 imports in check script This checks `ast.ImportFrom` statements to see if anything other than modules are being imported in this way. It enumerates all instances of this and suggests a fix. --- test/check_configurable_requirements.py | 69 +++++++++++++++++++++++-- 1 file changed, 66 insertions(+), 3 deletions(-) diff --git a/test/check_configurable_requirements.py b/test/check_configurable_requirements.py index 864c3e89e..ee1a54ce2 100644 --- a/test/check_configurable_requirements.py +++ b/test/check_configurable_requirements.py @@ -22,6 +22,7 @@ import inspect import logging import pkgutil import sys +import traceback import types from typing import Any, Iterator, List, Optional, Tuple, Type, Union @@ -105,6 +106,36 @@ class UnrequiredVersionableUsage(CodeViolation): ) +class DirectVolatilityImportUsage(CodeViolation): + + def __init__( + self, + module: types.ModuleType, + node: ast.AST, + importing_module: str, + imported_item: object, + imported_name: str, + ) -> None: + self.imported_item = imported_item + self.imported_name = imported_name + self.importing_module = importing_module + super().__init__(module, node) + + def __str__(self) -> str: + components = self.importing_module.split(".") + return ( + super().__str__() + + ": " + + ( + f"Direct import of {self.imported_name} " + f"({type(self.imported_item)}) " + f"from module {self.importing_module} - " + "change to " + f"'from {'.'.join(components[:-1])} import {components[-1]} and using {components[-1]}.{self.imported_name}" + ) + ) + + def is_versionable(var): try: return ( @@ -134,6 +165,39 @@ class ModuleVisitor(NodeVisitor): def violations(self): return self._violations + def enter_ImportFrom(self, node: ast.ImportFrom): + if not node.module: + return + + if ( + node.module + and node.module.startswith("volatility3") + and node.module != "volatility3.framework.constants._version" # make an exception for this + ): + for name in node.names: + try: + item = vars(self._module)[ + name.asname if name.asname is not None else name.name + ] + except KeyError: + logger.debug( + "Couldn't find imported name %s in module %s", + name.asname or name.name, + self._module.__name__, + ) + continue + + if not (isinstance(item, types.ModuleType) or inspect.isfunction(item)): + self._violations.append( + DirectVolatilityImportUsage( + self._module, + node, + node.module, + item, + name.asname or name.name, + ) + ) + def enter_ClassDef(self, node: ast.ClassDef) -> Any: logger.debug("Entering class %s", node.name) clazz = None @@ -304,6 +368,7 @@ def report_missing_requirements() -> Iterator[Tuple[str, UnrequiredVersionableUs modname, str(exc), ) + traceback.print_exc() continue logger.info("Checking module %s", plugin_module.__name__) @@ -344,9 +409,7 @@ def perform_review(): print(str(usage)) if found: - print( - f"Found {found} issues" - ) + print(f"Found {found} issues") sys.exit(1) print("All configurable classes passed validation!") From 47646c12d431707c2fb148a637289d79840998fa Mon Sep 17 00:00:00 2001 From: David McDonald Date: Fri, 28 Mar 2025 16:53:55 -0500 Subject: [PATCH 49/68] Framework: Fix all direct non-module imports This fixes all import from statements in the codebase that were importing things other than modules into module namespaces from other volatility3 modules. This should prevent accidental re-exporting. --- volatility3/framework/exceptions.py | 5 +- .../framework/interfaces/configuration.py | 5 +- volatility3/framework/interfaces/symbols.py | 3 +- volatility3/framework/layers/elf.py | 4 +- volatility3/framework/layers/intel.py | 16 +++--- volatility3/framework/layers/registry.py | 11 ++-- volatility3/framework/layers/xen.py | 4 +- volatility3/framework/plugins/linux/bash.py | 4 +- volatility3/framework/plugins/linux/elfs.py | 4 +- .../framework/plugins/linux/modxview.py | 10 ++-- .../framework/plugins/linux/sockstat.py | 23 ++++---- .../framework/plugins/linux/tracing/ftrace.py | 14 ++--- .../plugins/linux/tracing/tracepoints.py | 18 +++---- volatility3/framework/plugins/mac/bash.py | 4 +- .../framework/plugins/windows/dumpfiles.py | 13 +++-- volatility3/framework/plugins/windows/info.py | 9 ++-- .../framework/plugins/windows/pe_symbols.py | 4 +- .../framework/plugins/windows/psxview.py | 6 +-- .../plugins/windows/registry/hashdump.py | 28 +++++----- .../plugins/windows/registry/lsadump.py | 23 ++++---- .../plugins/windows/registry/printkey.py | 53 ++++++++++--------- .../windows/registry/scheduled_tasks.py | 2 +- .../plugins/windows/registry/userassist.py | 17 +++--- .../framework/plugins/windows/truecrypt.py | 29 +++++----- .../framework/symbols/linux/network.py | 4 +- .../symbols/windows/extensions/__init__.py | 7 +-- .../symbols/windows/extensions/network.py | 8 ++- .../symbols/windows/extensions/pool.py | 4 +- .../symbols/windows/extensions/registry.py | 38 +++++++------ .../symbols/windows/extensions/services.py | 4 +- .../framework/symbols/windows/pdbutil.py | 3 +- .../plugins/windows/registry/certificates.py | 3 +- 32 files changed, 190 insertions(+), 190 deletions(-) diff --git a/volatility3/framework/exceptions.py b/volatility3/framework/exceptions.py index a3d660444..34b41727a 100644 --- a/volatility3/framework/exceptions.py +++ b/volatility3/framework/exceptions.py @@ -11,7 +11,6 @@ size of the invalid page. from typing import Callable, Dict, Optional, Tuple from volatility3.framework import interfaces -from volatility3.framework.interfaces.configuration import VersionableInterface class VolatilityException(Exception): @@ -143,7 +142,7 @@ class VersionMismatchException(VolatilityException): def __init__( self, source_component: Callable, - target_component: VersionableInterface, + target_component: interfaces.configuration.VersionableInterface, target_version: Tuple[int, int, int], failure_reason: str = None, *args, @@ -151,7 +150,7 @@ class VersionMismatchException(VolatilityException): """ Args: source_component: The component that required the target component - target_component: The component that is required. Must inherit from VersionableInterface + target_component: The component that is required. Must inherit from interfaces.configuration.VersionableInterface target_version: The version of the target component that was required, and ultimately was not satisfied failure_reason: A detailed failure reason to enhance debugging and bug tracking """ diff --git a/volatility3/framework/interfaces/configuration.py b/volatility3/framework/interfaces/configuration.py index b6f4f889c..33d15d05e 100644 --- a/volatility3/framework/interfaces/configuration.py +++ b/volatility3/framework/interfaces/configuration.py @@ -37,7 +37,8 @@ from typing import ( Set, ) -from volatility3 import classproperty, framework +import volatility3 +from volatility3 import framework from volatility3.framework import constants, interfaces CONFIG_SEPARATOR = "." @@ -805,7 +806,7 @@ class VersionableInterface: framework.require_interface_version(*self._required_framework_version) super().__init__(*args, **kwargs) - @classproperty + @volatility3.classproperty def version(cls) -> Tuple[int, int, int]: """The version of the current interface (classmethods available on the component). diff --git a/volatility3/framework/interfaces/symbols.py b/volatility3/framework/interfaces/symbols.py index 2d142de9a..925be72c9 100644 --- a/volatility3/framework/interfaces/symbols.py +++ b/volatility3/framework/interfaces/symbols.py @@ -10,7 +10,6 @@ from typing import Any, Dict, Iterable, List, Mapping, Optional, Tuple, Type from volatility3.framework import constants, exceptions, interfaces from volatility3.framework.configuration import requirements from volatility3.framework.interfaces import configuration, objects -from volatility3.framework.interfaces.configuration import RequirementInterface class SymbolInterface: @@ -347,7 +346,7 @@ class SymbolTableInterface( return config @classmethod - def get_requirements(cls) -> List[RequirementInterface]: + def get_requirements(cls) -> List[configuration.RequirementInterface]: return super().get_requirements() + [ requirements.IntRequirement( name="symbol_mask", diff --git a/volatility3/framework/layers/elf.py b/volatility3/framework/layers/elf.py index 81f3c3634..5777981cb 100644 --- a/volatility3/framework/layers/elf.py +++ b/volatility3/framework/layers/elf.py @@ -6,7 +6,7 @@ import struct from typing import Optional from volatility3.framework import exceptions, interfaces, constants -from volatility3.framework.constants.linux import ELF_CLASS +from volatility3.framework.constants import linux as linux_constants from volatility3.framework.layers import segmented from volatility3.framework.symbols import intermed @@ -23,7 +23,7 @@ class Elf64Layer(segmented.SegmentedLayer): _header_struct = struct.Struct(" int: """Page shift for the intel memory layers.""" return cls._page_size_in_bits - @classproperty + @volatility3.classproperty @functools.lru_cache def page_size(cls) -> int: """Page size for the intel memory layers. @@ -90,30 +90,30 @@ class Intel(linear.LinearlyMappedLayer): """ return 1 << cls._page_size_in_bits - @classproperty + @volatility3.classproperty @functools.lru_cache def page_mask(cls) -> int: """Page mask for the intel memory layers.""" return ~(cls.page_size - 1) - @classproperty + @volatility3.classproperty @functools.lru_cache def bits_per_register(cls) -> int: """Returns the bits_per_register to determine the range of an IntelTranslationLayer.""" return cls._bits_per_register - @classproperty + @volatility3.classproperty @functools.lru_cache def minimum_address(cls) -> int: return 0 - @classproperty + @volatility3.classproperty @functools.lru_cache def maximum_address(cls) -> int: return (1 << cls._maxvirtaddr) - 1 - @classproperty + @volatility3.classproperty def structure(cls) -> List[Tuple[str, int, bool]]: return cls._structure diff --git a/volatility3/framework/layers/registry.py b/volatility3/framework/layers/registry.py index f324e24a0..e8b1246d3 100644 --- a/volatility3/framework/layers/registry.py +++ b/volatility3/framework/layers/registry.py @@ -7,11 +7,6 @@ from typing import Any, Callable, Dict, Iterable, List, Optional, Tuple, Union from volatility3.framework import constants, exceptions, interfaces, objects from volatility3.framework.configuration import requirements -from volatility3.framework.configuration.requirements import ( - IntRequirement, - TranslationLayerRequirement, -) -from volatility3.framework.exceptions import InvalidAddressException from volatility3.framework.layers import linear from volatility3.framework.symbols import intermed from volatility3.framework.symbols.windows import extensions @@ -154,7 +149,7 @@ class RegistryHive(linear.LinearlyMappedLayer): @property def root_cell_offset(self) -> int: """Returns the offset for the root cell in this hive.""" - with contextlib.suppress(InvalidAddressException): + with contextlib.suppress(exceptions.InvalidAddressException): if ( self._base_block.Signature.cast( "string", max_length=4, encoding="latin-1" @@ -271,7 +266,7 @@ class RegistryHive(linear.LinearlyMappedLayer): @classmethod def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]: return [ - IntRequirement( + requirements.IntRequirement( name="hive_offset", description="Offset within the base layer at which the hive lives", default=0, @@ -280,7 +275,7 @@ class RegistryHive(linear.LinearlyMappedLayer): requirements.SymbolTableRequirement( name="nt_symbols", description="Windows kernel symbols" ), - TranslationLayerRequirement( + requirements.TranslationLayerRequirement( name="base_layer", description="Layer in which the registry hive lives", optional=False, diff --git a/volatility3/framework/layers/xen.py b/volatility3/framework/layers/xen.py index c0a5e1a7d..7f42eb662 100644 --- a/volatility3/framework/layers/xen.py +++ b/volatility3/framework/layers/xen.py @@ -5,7 +5,7 @@ from typing import Optional from volatility3.framework import constants, interfaces, exceptions from volatility3.framework.layers import elf from volatility3.framework.symbols import intermed -from volatility3.framework.constants.linux import ELF_CLASS +from volatility3.framework.constants import linux as linux_constants vollog = logging.getLogger(__name__) @@ -15,7 +15,7 @@ class XenCoreDumpLayer(elf.Elf64Layer): _header_struct = struct.Struct(" ELF_MAX_EXTRACTION_SIZE: + if real_size < 0 or real_size > linux_constants.ELF_MAX_EXTRACTION_SIZE: raise ValueError(f"The claimed size of the ELF is invalid: {real_size}") sections[start] = real_size diff --git a/volatility3/framework/plugins/linux/modxview.py b/volatility3/framework/plugins/linux/modxview.py index ed21acfd1..c1707d26f 100644 --- a/volatility3/framework/plugins/linux/modxview.py +++ b/volatility3/framework/plugins/linux/modxview.py @@ -6,9 +6,9 @@ from typing import List, Dict, Iterator import volatility3.framework.symbols.linux.utilities.modules as linux_utilities_modules -from volatility3.framework import interfaces, deprecation +from volatility3.framework import interfaces, deprecation, renderers from volatility3.framework.configuration import requirements -from volatility3.framework.renderers import format_hints, TreeGrid, NotAvailableValue +from volatility3.framework.renderers import format_hints from volatility3.framework.symbols.linux import extensions from volatility3.framework.constants import architectures from volatility3.framework.symbols.linux.utilities import tainting @@ -156,12 +156,12 @@ spot modules presence and taints.""" yield ( 0, ( - module.get_name() or NotAvailableValue(), + module.get_name() or renderers.NotAvailableValue(), format_hints.Hex(module_offset), linux_utilities_modules.ModuleGathererLsmod.name in gatherers, linux_utilities_modules.ModuleGathererSysFs.name in gatherers, linux_utilities_modules.ModuleGathererScanner.name in gatherers, - taints or NotAvailableValue(), + taints or renderers.NotAvailableValue(), ), ) @@ -175,7 +175,7 @@ spot modules presence and taints.""" ("Taints", str), ] - return TreeGrid( + return renderers.TreeGrid( columns, self._generator(), ) diff --git a/volatility3/framework/plugins/linux/sockstat.py b/volatility3/framework/plugins/linux/sockstat.py index da5d8cb8c..a6acf825b 100644 --- a/volatility3/framework/plugins/linux/sockstat.py +++ b/volatility3/framework/plugins/linux/sockstat.py @@ -5,8 +5,8 @@ import logging from typing import Callable, Tuple, List, Dict -from volatility3.framework import interfaces, exceptions, constants, objects -from volatility3.framework.renderers import TreeGrid, NotAvailableValue, format_hints +from volatility3.framework import interfaces, exceptions, constants, objects, renderers +from volatility3.framework.renderers import format_hints from volatility3.framework.configuration import requirements from volatility3.framework.interfaces import plugins from volatility3.framework.objects import utility @@ -44,7 +44,7 @@ class SockHandlers(interfaces.configuration.VersionableInterface): try: netns_id = task.nsproxy.net_ns.get_inode() except AttributeError: - netns_id = NotAvailableValue() + netns_id = renderers.NotAvailableValue() self._netdevices = self._build_network_devices_map(netns_id) @@ -79,7 +79,7 @@ class SockHandlers(interfaces.configuration.VersionableInterface): ) for net_dev in net.dev_base_head.to_list(net_device_symname, "dev_list"): if ( - isinstance(netns_id, NotAvailableValue) + isinstance(netns_id, renderers.NotAvailableValue) or net.get_inode() != netns_id ): continue @@ -263,7 +263,7 @@ class SockHandlers(interfaces.configuration.VersionableInterface): # Kernel >= 3.7.10 src_port = netlink_sock.get_portid() except AttributeError: - src_port = NotAvailableValue() + src_port = renderers.NotAvailableValue() dst_addr = f"group:0x{netlink_sock.dst_group:08x}" module = netlink_sock.module @@ -273,7 +273,7 @@ class SockHandlers(interfaces.configuration.VersionableInterface): try: dst_port = netlink_sock.get_dst_portid() except AttributeError: - dst_port = NotAvailableValue() + dst_port = renderers.NotAvailableValue() state = netlink_sock.get_state() @@ -571,7 +571,7 @@ class Sockstat(plugins.PluginInterface): try: netns_id = net.get_inode() except AttributeError: - netns_id = NotAvailableValue() + netns_id = renderers.NotAvailableValue() yield task, netns_id, fd_num, family, sock_type, protocol, sock_fields @@ -586,10 +586,11 @@ class Sockstat(plugins.PluginInterface): `sock_stat` and `protocol` formatted. """ sock_stat = [ - NotAvailableValue() if field is None else str(field) for field in sock_stat + renderers.NotAvailableValue() if field is None else str(field) + for field in sock_stat ] if protocol is None: - protocol = NotAvailableValue() + protocol = renderers.NotAvailableValue() return tuple(sock_stat), protocol @@ -641,7 +642,7 @@ class Sockstat(plugins.PluginInterface): socket_filter_str = ( ",".join(f"{k}={v}" for k, v in extended.items()) if extended - else NotAvailableValue() + else renderers.NotAvailableValue() ) task_comm = utility.array_to_string(task.comm) @@ -685,6 +686,6 @@ class Sockstat(plugins.PluginInterface): ("Filter", str), ] - return TreeGrid( + return renderers.TreeGrid( tree_grid_args, self._generator(pids, netns_id, kernel_module_name) ) diff --git a/volatility3/framework/plugins/linux/tracing/ftrace.py b/volatility3/framework/plugins/linux/tracing/ftrace.py index c5e4f9ef8..afcc71784 100644 --- a/volatility3/framework/plugins/linux/tracing/ftrace.py +++ b/volatility3/framework/plugins/linux/tracing/ftrace.py @@ -10,9 +10,9 @@ from enum import Enum from dataclasses import dataclass import volatility3.framework.symbols.linux.utilities.modules as linux_utilities_modules -from volatility3.framework import constants, exceptions, interfaces +from volatility3.framework import constants, exceptions, interfaces, renderers from volatility3.framework.configuration import requirements -from volatility3.framework.renderers import format_hints, TreeGrid, NotAvailableValue +from volatility3.framework.renderers import format_hints from volatility3.framework.constants import architectures vollog = logging.getLogger(__name__) @@ -239,14 +239,14 @@ class CheckFtrace(interfaces.plugins.PluginInterface): ): formatted_results = ( format_hints.Hex(ftrace_ops_parsed.ftrace_ops_offset), - ftrace_ops_parsed.callback_symbol or NotAvailableValue(), + ftrace_ops_parsed.callback_symbol or renderers.NotAvailableValue(), format_hints.Hex(ftrace_ops_parsed.callback_address), - ftrace_ops_parsed.hooked_symbols or NotAvailableValue(), - ftrace_ops_parsed.module_name or NotAvailableValue(), + ftrace_ops_parsed.hooked_symbols or renderers.NotAvailableValue(), + ftrace_ops_parsed.module_name or renderers.NotAvailableValue(), ( format_hints.Hex(ftrace_ops_parsed.module_address) if ftrace_ops_parsed.module_address is not None - else NotAvailableValue() + else renderers.NotAvailableValue() ), ) if self.config["show_ftrace_flags"]: @@ -266,7 +266,7 @@ class CheckFtrace(interfaces.plugins.PluginInterface): if self.config.get("show_ftrace_flags"): columns.append(("Flags", str)) - return TreeGrid( + return renderers.TreeGrid( columns, self._generator(), ) diff --git a/volatility3/framework/plugins/linux/tracing/tracepoints.py b/volatility3/framework/plugins/linux/tracing/tracepoints.py index 9d4a4a2e3..25c87b664 100644 --- a/volatility3/framework/plugins/linux/tracing/tracepoints.py +++ b/volatility3/framework/plugins/linux/tracing/tracepoints.py @@ -5,15 +5,15 @@ # Public researches: https://i.blackhat.com/USA21/Wednesday-Handouts/us-21-Fixing-A-Memory-Forensics-Blind-Spot-Linux-Kernel-Tracing-wp.pdf import logging -from typing import Iterable, List, Optional from dataclasses import dataclass +from typing import Iterable, List, Optional import volatility3.framework.symbols.linux.utilities.modules as linux_utilities_modules -from volatility3.framework import constants, exceptions, interfaces +from volatility3.framework import constants, exceptions, interfaces, renderers from volatility3.framework.configuration import requirements -from volatility3.framework.renderers import format_hints, NotAvailableValue, TreeGrid -from volatility3.framework.objects import utility from volatility3.framework.constants import architectures +from volatility3.framework.objects import utility +from volatility3.framework.renderers import format_hints vollog = logging.getLogger(__name__) @@ -250,14 +250,14 @@ class CheckTracepoints(interfaces.plugins.PluginInterface): formatted_results = ( tracepoint_parsed.tracepoint_name, format_hints.Hex(tracepoint_parsed.tracepoint_address), - tracepoint_parsed.probe_name or NotAvailableValue(), + tracepoint_parsed.probe_name or renderers.NotAvailableValue(), format_hints.Hex(tracepoint_parsed.probe_address), - tracepoint_parsed.probe_priority or NotAvailableValue(), - tracepoint_parsed.module_name or NotAvailableValue(), + tracepoint_parsed.probe_priority or renderers.NotAvailableValue(), + tracepoint_parsed.module_name or renderers.NotAvailableValue(), ( format_hints.Hex(tracepoint_parsed.module_address) if tracepoint_parsed.module_address is not None - else NotAvailableValue() + else renderers.NotAvailableValue() ), ) yield ( @@ -276,7 +276,7 @@ class CheckTracepoints(interfaces.plugins.PluginInterface): ("Module address", format_hints.Hex), ] - return TreeGrid( + return renderers.TreeGrid( columns, self._generator(), ) diff --git a/volatility3/framework/plugins/mac/bash.py b/volatility3/framework/plugins/mac/bash.py index 5ad6facd0..ac10d4f4a 100644 --- a/volatility3/framework/plugins/mac/bash.py +++ b/volatility3/framework/plugins/mac/bash.py @@ -12,7 +12,7 @@ from volatility3.framework.configuration import requirements from volatility3.framework.interfaces import plugins from volatility3.framework.layers import scanners from volatility3.framework.objects import utility -from volatility3.framework.symbols.linux.bash import BashIntermedSymbols +from volatility3.framework.symbols.linux import bash from volatility3.plugins import timeliner from volatility3.plugins.mac import pslist @@ -68,7 +68,7 @@ class Bash(plugins.PluginInterface, timeliner.TimeLinerInterface): pack_format = "Q" bash_json_file = "bash64" - bash_table_name = BashIntermedSymbols.create( + bash_table_name = bash.BashIntermedSymbols.create( self.context, self.config_path, "linux", bash_json_file ) diff --git a/volatility3/framework/plugins/windows/dumpfiles.py b/volatility3/framework/plugins/windows/dumpfiles.py index e0adad2b1..e23b8ec3a 100755 --- a/volatility3/framework/plugins/windows/dumpfiles.py +++ b/volatility3/framework/plugins/windows/dumpfiles.py @@ -7,9 +7,14 @@ import ntpath import re from typing import List, Tuple, Type, Optional, Generator -from volatility3.framework import interfaces, renderers, exceptions, constants +from volatility3.framework import ( + interfaces, + exceptions, + constants, + renderers, +) from volatility3.framework.configuration import requirements -from volatility3.framework.renderers import format_hints, UnreadableValue +from volatility3.framework.renderers import format_hints from volatility3.plugins.windows import handles from volatility3.plugins.windows import pslist @@ -258,7 +263,7 @@ class DumpFiles(interfaces.plugins.PluginInterface): if file_re: name = file_obj.file_name_with_device() - if isinstance(name, UnreadableValue): + if isinstance(name, renderers.UnreadableValue): continue if not file_re.search(name): continue @@ -298,7 +303,7 @@ class DumpFiles(interfaces.plugins.PluginInterface): if file_re: name = file_obj.file_name_with_device() - if isinstance(name, UnreadableValue): + if isinstance(name, renderers.UnreadableValue): continue if not file_re.search(name): continue diff --git a/volatility3/framework/plugins/windows/info.py b/volatility3/framework/plugins/windows/info.py index a2e438c3f..3ff224c68 100644 --- a/volatility3/framework/plugins/windows/info.py +++ b/volatility3/framework/plugins/windows/info.py @@ -3,12 +3,11 @@ # import time -from typing import List, Tuple, Iterable +from typing import Iterable, List, Tuple -from volatility3.framework import constants, interfaces, layers, symbols +from volatility3.framework import constants, interfaces, layers, renderers, symbols from volatility3.framework.configuration import requirements from volatility3.framework.interfaces import plugins -from volatility3.framework.renderers import TreeGrid from volatility3.framework.symbols import intermed from volatility3.framework.symbols.windows.extensions import kdbg, pe @@ -294,4 +293,6 @@ class Info(plugins.PluginInterface): ) def run(self): - return TreeGrid([("Variable", str), ("Value", str)], self._generator()) + return renderers.TreeGrid( + [("Variable", str), ("Value", str)], self._generator() + ) diff --git a/volatility3/framework/plugins/windows/pe_symbols.py b/volatility3/framework/plugins/windows/pe_symbols.py index 3a08a1002..e3af0c28a 100644 --- a/volatility3/framework/plugins/windows/pe_symbols.py +++ b/volatility3/framework/plugins/windows/pe_symbols.py @@ -17,7 +17,7 @@ from volatility3.framework.symbols import intermed from volatility3.framework.symbols.windows import pdbutil from volatility3.framework.symbols.windows.extensions import pe from volatility3.plugins.windows import pslist, modules -from volatility3.framework.constants.windows import KERNEL_MODULE_NAMES +from volatility3.framework.constants import windows vollog = logging.getLogger(__name__) @@ -533,7 +533,7 @@ class PESymbols(interfaces.plugins.PluginInterface): # a `ntoskrnl.exe` can have an internal PDB name of any of the ones in the following list # The code attempts to find all possible PDBs to ensure the best chance of recovery if mod_name == PESymbols.os_module_name: - pdb_names = [fn + ".pdb" for fn in KERNEL_MODULE_NAMES] + pdb_names = [fn + ".pdb" for fn in windows.KERNEL_MODULE_NAMES] # for non-kernel files, replace the exe, sys, or dll extension with pdb else: diff --git a/volatility3/framework/plugins/windows/psxview.py b/volatility3/framework/plugins/windows/psxview.py index 7c3444f70..142987c3e 100644 --- a/volatility3/framework/plugins/windows/psxview.py +++ b/volatility3/framework/plugins/windows/psxview.py @@ -4,10 +4,10 @@ import string from itertools import chain from typing import Dict, Iterable, List -from volatility3.framework import constants, exceptions +from volatility3.framework import constants, exceptions, renderers from volatility3.framework.configuration import requirements from volatility3.framework.interfaces import plugins -from volatility3.framework.renderers import TreeGrid, format_hints +from volatility3.framework.renderers import format_hints from volatility3.framework.symbols.windows import extensions from volatility3.plugins.windows import ( handles, @@ -231,7 +231,7 @@ We recommend using -r pretty if you are looking at this plugin's output in a ter offset_type = "(Physical)" if self.config["physical-offsets"] else "(Virtual)" offset_str = "Offset" + offset_type - return TreeGrid( + return renderers.TreeGrid( [ (offset_str, format_hints.Hex), ("Name", str), diff --git a/volatility3/framework/plugins/windows/registry/hashdump.py b/volatility3/framework/plugins/windows/registry/hashdump.py index 630aa1cfd..1883d4530 100644 --- a/volatility3/framework/plugins/windows/registry/hashdump.py +++ b/volatility3/framework/plugins/windows/registry/hashdump.py @@ -11,8 +11,7 @@ from Crypto.Cipher import AES, ARC4, DES from volatility3.framework import interfaces, renderers, exceptions, constants from volatility3.framework.configuration import requirements -from volatility3.framework.exceptions import InvalidAddressException -from volatility3.framework.layers import registry as registrylayer +from volatility3.framework.layers import registry as registry_layer from volatility3.framework.symbols.windows.extensions import registry from volatility3.plugins.windows.registry import hivelist @@ -329,13 +328,13 @@ class Hashdump(interfaces.plugins.PluginInterface): @classmethod def get_hive_key( - cls, hive: registry.RegistryHive, key: str + cls, hive: registry_layer.RegistryHive, key: str ) -> Optional["registry.CM_KEY_NODE"]: result = None try: if hive: result = hive.get_key(key) - except (KeyError, registrylayer.RegistryException): + except (KeyError, registry_layer.RegistryException): vollog.info( f"Unable to load the required registry key {hive.get_name()}\\{key} from this memory image" ) @@ -343,7 +342,7 @@ class Hashdump(interfaces.plugins.PluginInterface): @classmethod def get_user_keys( - cls, samhive: registry.RegistryHive + cls, samhive: registry_layer.RegistryHive ) -> List[interfaces.objects.ObjectInterface]: user_key_path = "SAM\\Domains\\Account\\Users" @@ -354,7 +353,7 @@ class Hashdump(interfaces.plugins.PluginInterface): return [k for k in user_key.get_subkeys() if k.Name != "Names"] @classmethod - def get_bootkey(cls, syshive: registry.RegistryHive) -> Optional[bytes]: + def get_bootkey(cls, syshive: registry_layer.RegistryHive) -> Optional[bytes]: """ Returns the scrambled bootkey necesary to decrypt hashes """ @@ -382,8 +381,8 @@ class Hashdump(interfaces.plugins.PluginInterface): return None bootkey += class_data.decode("utf-16-le") except ( - InvalidAddressException, - registrylayer.RegistryException, + exceptions.InvalidAddressException, + registry_layer.RegistryException, ) as excp: vollog.log( constants.LOGLEVEL_VVV, f"Unable to read Lsa key {lk}: {excp}" @@ -398,7 +397,7 @@ class Hashdump(interfaces.plugins.PluginInterface): @classmethod def get_hbootkey( - cls, samhive: registry.RegistryHive, bootkey: bytes + cls, samhive: registry_layer.RegistryHive, bootkey: bytes ) -> Optional[bytes]: sam_account_path = "SAM\\Domains\\Account" @@ -456,7 +455,10 @@ class Hashdump(interfaces.plugins.PluginInterface): @classmethod def get_user_hashes( - cls, user: registry.CM_KEY_NODE, samhive: registry.RegistryHive, hbootkey: bytes + cls, + user: registry.CM_KEY_NODE, + samhive: registry_layer.RegistryHive, + hbootkey: bytes, ) -> Optional[Tuple[bytes, bytes]]: ## Will sometimes find extra user with rid = NAMES, returns empty strings right now try: @@ -470,7 +472,7 @@ class Hashdump(interfaces.plugins.PluginInterface): sam_data = samhive.read(v.Data + 4, v.DataLength) except ( exceptions.InvalidAddressException, - registrylayer.RegistryException, + registry_layer.RegistryException, ): return None @@ -570,7 +572,7 @@ class Hashdump(interfaces.plugins.PluginInterface): @classmethod def get_user_name( - cls, user: registry.CM_KEY_NODE, samhive: registry.RegistryHive + cls, user: registry.CM_KEY_NODE, samhive: registry_layer.RegistryHive ) -> Optional[bytes]: value = None for v in user.get_values(): @@ -593,7 +595,7 @@ class Hashdump(interfaces.plugins.PluginInterface): # replaces the dump_hashes method in vol2 def _generator( - self, syshive: registry.RegistryHive, samhive: registry.RegistryHive + self, syshive: registry_layer.RegistryHive, samhive: registry_layer.RegistryHive ): if syshive is None: vollog.debug("SYSTEM address is None: No system hive found") diff --git a/volatility3/framework/plugins/windows/registry/lsadump.py b/volatility3/framework/plugins/windows/registry/lsadump.py index 2154923ec..e394822d6 100644 --- a/volatility3/framework/plugins/windows/registry/lsadump.py +++ b/volatility3/framework/plugins/windows/registry/lsadump.py @@ -10,9 +10,8 @@ from Crypto.Cipher import ARC4, DES, AES from volatility3.framework import interfaces, renderers, exceptions from volatility3.framework.configuration import requirements -from volatility3.framework.exceptions import InvalidAddressException -from volatility3.framework.layers import registry +from volatility3.framework.layers import registry as registry_layers from volatility3.framework.symbols.windows import versions from volatility3.plugins.windows.registry import hashdump, hivelist from volatility3.framework.renderers import format_hints @@ -65,7 +64,7 @@ class Lsadump(interfaces.plugins.PluginInterface): @classmethod def get_lsa_key( - cls, sechive: registry.RegistryHive, bootkey: bytes, vista_or_later: bool + cls, sechive: registry_layers.RegistryHive, bootkey: bytes, vista_or_later: bool ) -> Optional[bytes]: if not bootkey: return None @@ -109,7 +108,7 @@ class Lsadump(interfaces.plugins.PluginInterface): @classmethod def get_secret_by_name( cls, - sechive: registry.RegistryHive, + sechive: registry_layers.RegistryHive, name: str, lsakey: bytes, is_vista_or_later: bool, @@ -123,8 +122,8 @@ class Lsadump(interfaces.plugins.PluginInterface): try: enc_secret_value = next(enc_secret_key.get_values(), None) except ( - InvalidAddressException, - registry.RegistryException, + exceptions.InvalidAddressException, + registry_layers.RegistryException, ): enc_secret_value = None @@ -171,7 +170,9 @@ class Lsadump(interfaces.plugins.PluginInterface): return decrypted_data[8 : 8 + dec_data_len] def _generator( - self, syshive: registry.RegistryHive, sechive: registry.RegistryHive + self, + syshive: registry_layers.RegistryHive, + sechive: registry_layers.RegistryHive, ): kernel = self.context.modules[self.config["kernel"]] @@ -206,8 +207,8 @@ class Lsadump(interfaces.plugins.PluginInterface): enc_secret_value = next(sec_val_key.get_values(), None) except ( StopIteration, - InvalidAddressException, - registry.RegistryException, + exceptions.InvalidAddressException, + registry_layers.RegistryException, ): enc_secret_value = None @@ -229,8 +230,8 @@ class Lsadump(interfaces.plugins.PluginInterface): try: key_name = key.get_name() except ( - InvalidAddressException, - registry.RegistryException, + exceptions.InvalidAddressException, + registry_layers.RegistryException, ): key_name = renderers.UnreadableValue() diff --git a/volatility3/framework/plugins/windows/registry/printkey.py b/volatility3/framework/plugins/windows/registry/printkey.py index 6ca56b1bb..ab9a0392d 100644 --- a/volatility3/framework/plugins/windows/registry/printkey.py +++ b/volatility3/framework/plugins/windows/registry/printkey.py @@ -4,18 +4,13 @@ import datetime import logging -from typing import List, Optional, Sequence, Iterable, Tuple, Union +from typing import Iterable, List, Optional, Sequence, Tuple, Union -from volatility3.framework import objects, renderers, exceptions, interfaces, constants +from volatility3.framework import constants, exceptions, interfaces, objects, renderers from volatility3.framework.configuration import requirements -from volatility3.framework.layers.registry import ( - RegistryHive, - RegistryFormatException, - InvalidAddressException, - RegistryException, -) -from volatility3.framework.renderers import TreeGrid, conversion, format_hints -from volatility3.framework.symbols.windows.extensions.registry import RegValueTypes +from volatility3.framework.layers import registry as registry_layer +from volatility3.framework.renderers import conversion, format_hints +from volatility3.framework.symbols.windows.extensions import registry from volatility3.plugins.windows.registry import hivelist vollog = logging.getLogger(__name__) @@ -55,7 +50,7 @@ class PrintKey(interfaces.plugins.PluginInterface): @classmethod def key_iterator( cls, - hive: RegistryHive, + hive: registry_layer.RegistryHive, node_path: Optional[Sequence[objects.StructType]] = None, recurse: bool = False, ) -> Iterable[ @@ -87,14 +82,14 @@ class PrintKey(interfaces.plugins.PluginInterface): try: key_path_names.append(k.get_name()) except ( - InvalidAddressException, - RegistryException, + registry_layer.InvalidAddressException, + registry_layer.RegistryException, ): key_path_names.append("-") key_path = "\\".join([k for k in key_path_names]) if node.vol.type_name.endswith(constants.BANG + "_CELL_DATA"): - raise RegistryFormatException( + raise registry_layer.RegistryFormatException( hive.name, "Encountered _CELL_DATA instead of _CM_KEY_NODE" ) last_write_time = conversion.wintime_to_datetime(node.LastWriteTime.QuadPart) @@ -116,7 +111,7 @@ class PrintKey(interfaces.plugins.PluginInterface): key_node.get_name() except ( exceptions.InvalidAddressException, - RegistryException, + registry_layer.RegistryException, ) as excp: vollog.debug(excp) continue @@ -138,7 +133,7 @@ class PrintKey(interfaces.plugins.PluginInterface): def _printkey_iterator( self, - hive: RegistryHive, + hive: registry_layer.RegistryHive, node_path: Optional[Sequence[objects.StructType]] = None, recurse: bool = False, ): @@ -166,7 +161,7 @@ class PrintKey(interfaces.plugins.PluginInterface): key_node_name = node.get_name() except ( exceptions.InvalidAddressException, - RegistryException, + registry_layer.RegistryException, ) as excp: vollog.debug(excp) key_node_name = renderers.UnreadableValue() @@ -193,16 +188,16 @@ class PrintKey(interfaces.plugins.PluginInterface): value_node_name = node.get_name() or "(Default)" except ( exceptions.InvalidAddressException, - RegistryException, + registry_layer.RegistryException, ) as excp: vollog.debug(excp) value_node_name = renderers.UnreadableValue() try: - value_type = RegValueTypes(node.Type).name + value_type = registry.RegValueTypes(node.Type).name except ( exceptions.InvalidAddressException, - RegistryException, + registry_layer.RegistryException, ) as excp: vollog.debug(excp) value_type = renderers.UnreadableValue() @@ -222,11 +217,17 @@ class PrintKey(interfaces.plugins.PluginInterface): value_data = format_hints.MultiTypeData( value_data, encoding="utf-8" ) - elif RegValueTypes(node.Type) == RegValueTypes.REG_BINARY: + elif ( + registry.RegValueTypes(node.Type) + == registry.RegValueTypes.REG_BINARY + ): value_data = format_hints.MultiTypeData( value_data, show_hex=True ) - elif RegValueTypes(node.Type) == RegValueTypes.REG_MULTI_SZ: + elif ( + registry.RegValueTypes(node.Type) + == registry.RegValueTypes.REG_MULTI_SZ + ): value_data = format_hints.MultiTypeData( value_data, encoding="utf-16-le", split_nulls=True ) @@ -237,7 +238,7 @@ class PrintKey(interfaces.plugins.PluginInterface): except ( ValueError, exceptions.InvalidAddressException, - RegistryException, + registry_layer.RegistryException, ) as excp: vollog.debug(excp) value_data = renderers.UnreadableValue() @@ -279,13 +280,13 @@ class PrintKey(interfaces.plugins.PluginInterface): except ( exceptions.InvalidAddressException, KeyError, - RegistryException, + registry_layer.RegistryException, ) as excp: if isinstance(excp, KeyError): vollog.debug( f"Key '{key}' not found in Hive at offset {hex(hive.hive_offset)}." ) - elif isinstance(excp, RegistryException): + elif isinstance(excp, registry_layer.RegistryException): vollog.debug(excp) elif isinstance(excp, exceptions.InvalidAddressException): vollog.debug( @@ -308,7 +309,7 @@ class PrintKey(interfaces.plugins.PluginInterface): def run(self): offset = self.config.get("offset", None) - return TreeGrid( + return renderers.TreeGrid( columns=[ ("Last Write Time", datetime.datetime), ("Hive Offset", format_hints.Hex), diff --git a/volatility3/framework/plugins/windows/registry/scheduled_tasks.py b/volatility3/framework/plugins/windows/registry/scheduled_tasks.py index 09ebed7b9..a2789e5df 100644 --- a/volatility3/framework/plugins/windows/registry/scheduled_tasks.py +++ b/volatility3/framework/plugins/windows/registry/scheduled_tasks.py @@ -1216,7 +1216,7 @@ class ScheduledTasks(interfaces.plugins.PluginInterface, timeliner.TimeLinerInte @classmethod def _get_task_keys( - cls, software_hive: reg_extensions.RegistryHive + cls, software_hive: registry.RegistryHive ) -> Tuple[ Optional[reg_extensions.CM_KEY_NODE], Optional[reg_extensions.CM_KEY_NODE] ]: diff --git a/volatility3/framework/plugins/windows/registry/userassist.py b/volatility3/framework/plugins/windows/registry/userassist.py index 809d0b2b3..3272b241c 100644 --- a/volatility3/framework/plugins/windows/registry/userassist.py +++ b/volatility3/framework/plugins/windows/registry/userassist.py @@ -12,11 +12,8 @@ from typing import Any, Generator, List, Tuple from volatility3.framework import constants, exceptions, interfaces, renderers from volatility3.framework.configuration import requirements -from volatility3.framework.layers.physical import BufferDataLayer -from volatility3.framework.layers.registry import ( - RegistryHive, - RegistryException, -) +from volatility3.framework.layers import physical +from volatility3.framework.layers import registry as registry_layers from volatility3.framework.renderers import conversion, format_hints from volatility3.framework.symbols import intermed from volatility3.plugins.windows.registry import hivelist @@ -94,7 +91,7 @@ class UserAssist(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterfac return item userassist_layer_name = self.context.layers.free_layer_name("userassist_buffer") - buffer = BufferDataLayer( + buffer = physical.BufferDataLayer( self.context, self._config_path, userassist_layer_name, userassist_data ) self.context.add_layer(buffer) @@ -158,7 +155,7 @@ class UserAssist(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterfac ).has_member("CookiePad") def list_userassist( - self, hive: RegistryHive + self, hive: registry_layers.RegistryHive ) -> Generator[Tuple[int, Tuple], None, None]: """Generate userassist data for a registry hive.""" @@ -180,7 +177,7 @@ class UserAssist(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterfac "software\\microsoft\\windows\\currentversion\\explorer\\userassist", return_list=True, ) - except RegistryException as e: + except registry_layers.RegistryException as e: vollog.warning( f"Error accessing UserAssist key in {hive_name} at {hive.hive_offset:#x}: {e}" ) @@ -250,7 +247,7 @@ class UserAssist(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterfac subkey_name = subkey.get_name() except ( exceptions.InvalidAddressException, - RegistryException, + registry_layers.RegistryException, ): subkey_name = renderers.UnreadableValue() @@ -279,7 +276,7 @@ class UserAssist(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterfac value_name = value.get_name() except ( exceptions.InvalidAddressException, - RegistryException, + registry_layers.RegistryException, ): value_name = renderers.UnreadableValue() diff --git a/volatility3/framework/plugins/windows/truecrypt.py b/volatility3/framework/plugins/windows/truecrypt.py index aaab49d20..0478e37a5 100644 --- a/volatility3/framework/plugins/windows/truecrypt.py +++ b/volatility3/framework/plugins/windows/truecrypt.py @@ -3,20 +3,15 @@ # import logging +from typing import Generator, Iterable, List, Tuple -from typing import Iterable, Generator, List, Tuple - -from volatility3.framework import constants, interfaces, renderers +from volatility3.framework import constants, interfaces, objects, renderers from volatility3.framework.configuration import requirements -from volatility3.framework.interfaces.configuration import RequirementInterface -from volatility3.framework.interfaces.objects import ObjectInterface -from volatility3.framework.objects import Bytes, DataFormatInfo, Integer, StructType -from volatility3.framework.objects.templates import ObjectTemplate +from volatility3.framework.interfaces import configuration from volatility3.framework.objects.utility import array_to_string 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 modules vollog = logging.getLogger(__name__) @@ -29,7 +24,7 @@ class Passphrase(interfaces.plugins.PluginInterface): _required_framework_version = (2, 5, 2) @classmethod - def get_requirements(cls) -> List[RequirementInterface]: + def get_requirements(cls) -> List[configuration.RequirementInterface]: return [ requirements.ModuleRequirement( "kernel", @@ -67,7 +62,7 @@ class Passphrase(interfaces.plugins.PluginInterface): layer_name, module_base, ) - data_section: StructType = next( + data_section: objects.StructType = next( sec for sec in dos_header.get_nt_header().get_sections() if array_to_string(sec.Name) == ".data" @@ -76,11 +71,11 @@ class Passphrase(interfaces.plugins.PluginInterface): size: int = data_section.Misc.VirtualSize # Looking at `Length` in TrueCrypt/Common/Password.h::Password struct DWORD_SIZE_BYTES: int = 4 - format = DataFormatInfo( + format = objects.DataFormatInfo( length=DWORD_SIZE_BYTES, byteorder="little", signed=True ) - int32 = ObjectTemplate( - Integer, pe_table_name + constants.BANG + "int", data_format=format + int32 = objects.templates.ObjectTemplate( + objects.Integer, pe_table_name + constants.BANG + "int", data_format=format ) count, not_aligned = divmod(size, DWORD_SIZE_BYTES) if not_aligned: @@ -99,7 +94,7 @@ class Passphrase(interfaces.plugins.PluginInterface): if not min_length <= length <= 64: continue offset = length.vol["offset"] + DWORD_SIZE_BYTES - passphrase: Bytes = self.context.object( + passphrase: objects.Bytes = self.context.object( pe_table_name + constants.BANG + "bytes", layer_name, offset, @@ -111,7 +106,7 @@ class Passphrase(interfaces.plugins.PluginInterface): continue # TrueCrypt/Common/Password.h::Password struct is padded with # 3 zero bytes to keep 64-byte alignment. - buf: Bytes = self.context.object( + buf: objects.Bytes = self.context.object( pe_table_name + constants.BANG + "bytes", layer_name, offset + length + 1, # +1 for '\0'-terminated password string @@ -124,8 +119,8 @@ class Passphrase(interfaces.plugins.PluginInterface): def _generator(self): kernel = self.context.modules[self.config["kernel"]] - mods: Iterable[ObjectInterface] = modules.Modules.list_modules( - self.context, self.config["kernel"] + mods: Iterable[interfaces.objects.ObjectInterface] = ( + modules.Modules.list_modules(self.context, self.config["kernel"]) ) try: truecrypt_module_base = next( diff --git a/volatility3/framework/symbols/linux/network.py b/volatility3/framework/symbols/linux/network.py index c88e6fc69..72ffe8047 100644 --- a/volatility3/framework/symbols/linux/network.py +++ b/volatility3/framework/symbols/linux/network.py @@ -1,9 +1,9 @@ from volatility3.framework.symbols import intermed from volatility3.framework.symbols.linux.extensions import network -from volatility3.framework.interfaces.configuration import VersionableInterface +from volatility3.framework.interfaces import configuration -class NetSymbols(VersionableInterface): +class NetSymbols(configuration.VersionableInterface): _version = (1, 0, 0) @classmethod diff --git a/volatility3/framework/symbols/windows/extensions/__init__.py b/volatility3/framework/symbols/windows/extensions/__init__.py index 75608cfc6..9fe250ba5 100755 --- a/volatility3/framework/symbols/windows/extensions/__init__.py +++ b/volatility3/framework/symbols/windows/extensions/__init__.py @@ -18,7 +18,6 @@ from volatility3.framework import ( renderers, symbols, ) -from volatility3.framework.interfaces.objects import ObjectInterface from volatility3.framework.layers import intel from volatility3.framework.objects import utility from volatility3.framework.renderers import conversion @@ -413,7 +412,9 @@ class DEVICE_OBJECT(objects.StructType, pool.ExecutiveObject): header = self.get_object_header() return header.NameInfo.Name.String # type: ignore - def get_attached_devices(self) -> Generator[ObjectInterface, None, None]: + def get_attached_devices( + self, + ) -> Generator[interfaces.objects.ObjectInterface, None, None]: """Enumerate the attached device's objects""" seen = set() @@ -443,7 +444,7 @@ class DRIVER_OBJECT(objects.StructType, pool.ExecutiveObject): header = self.get_object_header() return header.NameInfo.Name.String # type: ignore - def get_devices(self) -> Generator[ObjectInterface, None, None]: + def get_devices(self) -> Generator[interfaces.objects.ObjectInterface, None, None]: """Enumerate the driver's device objects""" seen = set() diff --git a/volatility3/framework/symbols/windows/extensions/network.py b/volatility3/framework/symbols/windows/extensions/network.py index e41ac6a05..62cb4fba4 100644 --- a/volatility3/framework/symbols/windows/extensions/network.py +++ b/volatility3/framework/symbols/windows/extensions/network.py @@ -4,17 +4,15 @@ import logging import socket -from typing import Dict, Tuple, List, Union, Optional +from typing import Dict, List, Optional, Tuple, Union -from volatility3.framework import exceptions -from volatility3.framework import objects, interfaces -from volatility3.framework.objects import Array +from volatility3.framework import exceptions, interfaces, objects from volatility3.framework.renderers import conversion vollog = logging.getLogger(__name__) -def inet_ntop(address_family: int, packed_ip: Union[List[int], Array]) -> str: +def inet_ntop(address_family: int, packed_ip: Union[List[int], objects.Array]) -> str: if address_family in [socket.AF_INET6, socket.AF_INET]: try: return socket.inet_ntop(address_family, bytes(packed_ip)) diff --git a/volatility3/framework/symbols/windows/extensions/pool.py b/volatility3/framework/symbols/windows/extensions/pool.py index b0c480d19..f12182fa7 100644 --- a/volatility3/framework/symbols/windows/extensions/pool.py +++ b/volatility3/framework/symbols/windows/extensions/pool.py @@ -4,7 +4,7 @@ import logging import struct from typing import Dict, List, Optional, Tuple, Union -from volatility3.plugins.windows.poolscanner import PoolConstraint +from volatility3.plugins.windows import poolscanner from volatility3.framework import ( constants, @@ -28,7 +28,7 @@ class POOL_HEADER(objects.StructType): def get_object( self, - constraint: PoolConstraint, + constraint: poolscanner.PoolConstraint, use_top_down: bool, kernel_symbol_table: Optional[str] = None, native_layer_name: Optional[str] = None, diff --git a/volatility3/framework/symbols/windows/extensions/registry.py b/volatility3/framework/symbols/windows/extensions/registry.py index 987f01ac1..e3419fab0 100644 --- a/volatility3/framework/symbols/windows/extensions/registry.py +++ b/volatility3/framework/symbols/windows/extensions/registry.py @@ -8,10 +8,7 @@ import struct from typing import Iterator, Optional, Union, cast from volatility3.framework import constants, exceptions, interfaces, objects -from volatility3.framework.layers.registry import ( - RegistryException, - RegistryHive, -) +from volatility3.framework.layers import registry vollog = logging.getLogger(__name__) @@ -102,7 +99,9 @@ class CMHIVE(objects.StructType): for attr in ["FileFullPath", "FileUserName", "HiveRootPath"]: with contextlib.suppress( - AttributeError, exceptions.InvalidAddressException, RegistryException + AttributeError, + exceptions.InvalidAddressException, + registry.RegistryException, ): name = getattr(self, attr) if name.Length > 0: @@ -172,7 +171,9 @@ class CM_KEY_NODE(objects.StructType): Raises TypeError if the key was not instantiated on a RegistryHive layer """ - if not isinstance(self._context.layers[self.vol.layer_name], RegistryHive): + if not isinstance( + self._context.layers[self.vol.layer_name], registry.RegistryHive + ): raise TypeError("CM_KEY_NODE was not instantiated on a RegistryHive layer") return bool(self.vol.offset & 0x80000000) @@ -182,7 +183,7 @@ class CM_KEY_NODE(objects.StructType): Raises TypeError if the key was not instantiated on a RegistryHive layer """ hive = self._context.layers[self.vol.layer_name] - if not isinstance(hive, RegistryHive): + if not isinstance(hive, registry.RegistryHive): raise TypeError("CM_KEY_NODE was not instantiated on a RegistryHive layer") for index in range(2): # Use get_cell because it should *always* be a KeyIndex @@ -190,7 +191,7 @@ class CM_KEY_NODE(objects.StructType): yield from self._get_subkeys_recursive(hive, subkey_node) def _get_subkeys_recursive( - self, hive: RegistryHive, node: interfaces.objects.ObjectInterface + self, hive: registry.RegistryHive, node: interfaces.objects.ObjectInterface ) -> Iterator["CM_KEY_NODE"]: """Recursively descend a node returning subkeys.""" # The keylist appears to include 4 bytes of key name after each value @@ -200,7 +201,7 @@ class CM_KEY_NODE(objects.StructType): signature = node.cast("string", max_length=2, encoding="latin-1") except ( exceptions.InvalidAddressException, - RegistryException, + registry.RegistryException, ): return None @@ -229,7 +230,7 @@ class CM_KEY_NODE(objects.StructType): subnode = hive.get_node(subnode_offset) except ( exceptions.InvalidAddressException, - RegistryException, + registry.RegistryException, ): vollog.log( constants.LOGLEVEL_VVV, @@ -244,7 +245,7 @@ class CM_KEY_NODE(objects.StructType): Raises TypeError if the key was not instantiated on a RegistryHive layer """ hive = self._context.layers[self.vol.layer_name] - if not isinstance(hive, RegistryHive): + if not isinstance(hive, registry.RegistryHive): raise TypeError("CM_KEY_NODE was not instantiated on a RegistryHive layer") try: @@ -255,7 +256,7 @@ class CM_KEY_NODE(objects.StructType): if v != 0: try: node = hive.get_node(v) - except (RegistryException,) as excp: + except (registry.RegistryException,) as excp: vollog.debug(f"Invalid address {excp}") continue if isinstance(node, CM_KEY_VALUE): @@ -263,7 +264,7 @@ class CM_KEY_NODE(objects.StructType): except ( exceptions.InvalidAddressException, - RegistryException, + registry.RegistryException, ) as excp: vollog.debug(f"Invalid address in get_values iteration: {excp}") return None @@ -281,7 +282,7 @@ class CM_KEY_NODE(objects.StructType): Raises TypeError if the key was not instantiated on a RegistryHive layer """ reg = self._context.layers[self.vol.layer_name] - if not isinstance(reg, RegistryHive): + if not isinstance(reg, registry.RegistryHive): raise TypeError("Key was not instantiated on a RegistryHive layer") # Using the offset adds a significant delay (since it cannot be cached easily) # if self.vol.offset == reg.get_node(reg.root_cell_offset).vol.offset: @@ -320,7 +321,7 @@ class CM_KEY_VALUE(objects.StructType): data = b"" # Check if the data is stored inline layer = self._context.layers[self.vol.layer_name] - if not isinstance(layer, RegistryHive): + if not isinstance(layer, registry.RegistryHive): raise TypeError("Key value was not instantiated on a RegistryHive layer") # If the high-bit is set @@ -353,7 +354,10 @@ class CM_KEY_VALUE(objects.StructType): offset=layer.get_cell(block_offset).vol.offset, length=amount, ) - except (exceptions.InvalidAddressException, RegistryException): + except ( + exceptions.InvalidAddressException, + registry.RegistryException, + ): vollog.debug( f"Failed to read {amount:x} bytes of data, padding with {amount:x}" ) @@ -363,7 +367,7 @@ class CM_KEY_VALUE(objects.StructType): # but the length at the start could be negative so just adding 4 to jump past it try: data = layer.read(self.Data + 4, datalen) - except (exceptions.InvalidAddressException, RegistryException): + except (exceptions.InvalidAddressException, registry.RegistryException): vollog.debug( f"Failed to read {datalen:x} bytes of data, returning {datalen:x} null bytes" ) diff --git a/volatility3/framework/symbols/windows/extensions/services.py b/volatility3/framework/symbols/windows/extensions/services.py index 0a2194e07..9f36a1b9c 100644 --- a/volatility3/framework/symbols/windows/extensions/services.py +++ b/volatility3/framework/symbols/windows/extensions/services.py @@ -4,7 +4,7 @@ from volatility3.framework import objects, interfaces from volatility3.framework import exceptions -from volatility3.framework.symbols.wrappers import Flags +from volatility3.framework.symbols import wrappers from volatility3.framework import renderers from typing import Union @@ -91,7 +91,7 @@ class SERVICE_RECORD(objects.StructType): "SERVICE_INTERACTIVE_PROCESS": 256, } - type_flags = Flags(choices=SERVICE_TYPE_FLAGS) + type_flags = wrappers.Flags(choices=SERVICE_TYPE_FLAGS) return "|".join(type_flags(self.Type)) def traverse(self): diff --git a/volatility3/framework/symbols/windows/pdbutil.py b/volatility3/framework/symbols/windows/pdbutil.py index 3c23eddb8..5f5c8cac8 100644 --- a/volatility3/framework/symbols/windows/pdbutil.py +++ b/volatility3/framework/symbols/windows/pdbutil.py @@ -16,7 +16,6 @@ from volatility3 import symbols from volatility3.framework import constants, contexts, exceptions, interfaces from volatility3.framework.automagic import symbol_cache from volatility3.framework.configuration import requirements -from volatility3.framework.configuration.requirements import SymbolTableRequirement from volatility3.framework.symbols import intermed from volatility3.framework.symbols.windows import pdbconv @@ -140,7 +139,7 @@ class PDBUtility(interfaces.configuration.VersionableInterface): requirement_name = interfaces.configuration.path_head(config_path) # Construct the appropriate symbol table - requirement = SymbolTableRequirement( + requirement = requirements.SymbolTableRequirement( name=requirement_name, description="PDBUtility generated symbol table" ) requirement.construct(context, parent_config_path) diff --git a/volatility3/plugins/windows/registry/certificates.py b/volatility3/plugins/windows/registry/certificates.py index caf244f95..d96284036 100644 --- a/volatility3/plugins/windows/registry/certificates.py +++ b/volatility3/plugins/windows/registry/certificates.py @@ -4,6 +4,7 @@ import struct from typing import Iterator, List, Optional, Tuple, Type from volatility3.framework import exceptions, interfaces, renderers +from volatility3.framework.layers import registry as registry_layer from volatility3.framework.configuration import requirements from volatility3.framework.symbols.windows.extensions import registry from volatility3.plugins.windows.registry import hivelist, printkey @@ -80,7 +81,7 @@ class Certificates(interfaces.plugins.PluginInterface): ]: with contextlib.suppress( KeyError, - registry.RegistryException, + registry_layer.RegistryException, exceptions.InvalidAddressException, ): # Walk it From a3353a3cb60ea55a7893cb28488cb2465f67224e Mon Sep 17 00:00:00 2001 From: David McDonald Date: Fri, 28 Mar 2025 16:57:15 -0500 Subject: [PATCH 50/68] CI Testing: Renames script and updates job name --- .../{check-requirements.yml => vol3-code-analysis.yml} | 5 ++--- ...igurable_requirements.py => volatility3_code_analysis.py} | 0 2 files changed, 2 insertions(+), 3 deletions(-) rename .github/workflows/{check-requirements.yml => vol3-code-analysis.yml} (73%) rename test/{check_configurable_requirements.py => volatility3_code_analysis.py} (100%) diff --git a/.github/workflows/check-requirements.yml b/.github/workflows/vol3-code-analysis.yml similarity index 73% rename from .github/workflows/check-requirements.yml rename to .github/workflows/vol3-code-analysis.yml index 9892d7b94..fc2b297fd 100644 --- a/.github/workflows/check-requirements.yml +++ b/.github/workflows/vol3-code-analysis.yml @@ -1,4 +1,4 @@ -name: Check Volatility3 Version Requirements +name: Volatility3 Code Analysis on: [push, pull_request] jobs: @@ -21,5 +21,4 @@ jobs: - name: Testing... run: | - # Verify completeness of ConfigurableInterface requirements - python ./test/check_configurable_requirements.py + python ./test/volatility3_code_analysis.py diff --git a/test/check_configurable_requirements.py b/test/volatility3_code_analysis.py similarity index 100% rename from test/check_configurable_requirements.py rename to test/volatility3_code_analysis.py From f72b717c0001426abfcb50e6a37717df4a80ddf7 Mon Sep 17 00:00:00 2001 From: David McDonald Date: Fri, 28 Mar 2025 22:25:52 -0500 Subject: [PATCH 51/68] Comment type annotation to fix circular import --- volatility3/framework/symbols/windows/extensions/registry.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/volatility3/framework/symbols/windows/extensions/registry.py b/volatility3/framework/symbols/windows/extensions/registry.py index e3419fab0..e18a15cba 100644 --- a/volatility3/framework/symbols/windows/extensions/registry.py +++ b/volatility3/framework/symbols/windows/extensions/registry.py @@ -191,7 +191,7 @@ class CM_KEY_NODE(objects.StructType): yield from self._get_subkeys_recursive(hive, subkey_node) def _get_subkeys_recursive( - self, hive: registry.RegistryHive, node: interfaces.objects.ObjectInterface + self, hive: "registry.RegistryHive", node: interfaces.objects.ObjectInterface ) -> Iterator["CM_KEY_NODE"]: """Recursively descend a node returning subkeys.""" # The keylist appears to include 4 bytes of key name after each value From 296cb3c1131c7379d27668da7d5cfa9278a6eb79 Mon Sep 17 00:00:00 2001 From: David McDonald Date: Sat, 29 Mar 2025 15:48:24 -0500 Subject: [PATCH 52/68] Code Analysis: Give pass to 'volatility3' Also moves some code into a private method with a docstring in the visitor class. --- test/volatility3_code_analysis.py | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/test/volatility3_code_analysis.py b/test/volatility3_code_analysis.py index ee1a54ce2..100ad3074 100644 --- a/test/volatility3_code_analysis.py +++ b/test/volatility3_code_analysis.py @@ -165,13 +165,16 @@ class ModuleVisitor(NodeVisitor): def violations(self): return self._violations - def enter_ImportFrom(self, node: ast.ImportFrom): - if not node.module: - return - + def _check_vol3_import_from(self, node: ast.ImportFrom): + """ + Ensure that the only thing imported from a volatility3 module (apart + from the root volatility3 module) are functions and modules. This + prevents re-exporting of classes and variables from modules that use + them. + """ if ( node.module - and node.module.startswith("volatility3") + and node.module.startswith("volatility3.") # Give a pass to volatility3 module and node.module != "volatility3.framework.constants._version" # make an exception for this ): for name in node.names: @@ -198,6 +201,10 @@ class ModuleVisitor(NodeVisitor): ) ) + def enter_ImportFrom(self, node: ast.ImportFrom): + self._check_vol3_import_from(node) + + def enter_ClassDef(self, node: ast.ClassDef) -> Any: logger.debug("Entering class %s", node.name) clazz = None From ee3d965ef6cffe9c1011124f0c4bbf9cd64d7173 Mon Sep 17 00:00:00 2001 From: David McDonald Date: Sat, 29 Mar 2025 15:50:36 -0500 Subject: [PATCH 53/68] Revert changes to configuration.py --- volatility3/framework/interfaces/configuration.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/volatility3/framework/interfaces/configuration.py b/volatility3/framework/interfaces/configuration.py index 33d15d05e..b6f4f889c 100644 --- a/volatility3/framework/interfaces/configuration.py +++ b/volatility3/framework/interfaces/configuration.py @@ -37,8 +37,7 @@ from typing import ( Set, ) -import volatility3 -from volatility3 import framework +from volatility3 import classproperty, framework from volatility3.framework import constants, interfaces CONFIG_SEPARATOR = "." @@ -806,7 +805,7 @@ class VersionableInterface: framework.require_interface_version(*self._required_framework_version) super().__init__(*args, **kwargs) - @volatility3.classproperty + @classproperty def version(cls) -> Tuple[int, int, int]: """The version of the current interface (classmethods available on the component). From c17bcb644ba9a3b1750d84856619902dd79b034a Mon Sep 17 00:00:00 2001 From: David McDonald Date: Sat, 29 Mar 2025 15:51:19 -0500 Subject: [PATCH 54/68] Revert changes to intel.py --- volatility3/framework/layers/intel.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/volatility3/framework/layers/intel.py b/volatility3/framework/layers/intel.py index a79d1ef5c..1069b7f6d 100644 --- a/volatility3/framework/layers/intel.py +++ b/volatility3/framework/layers/intel.py @@ -9,7 +9,7 @@ import math import struct from typing import Any, Dict, Iterable, List, Optional, Tuple -import volatility3 +from volatility3 import classproperty from volatility3.framework import exceptions, interfaces, constants from volatility3.framework.configuration import requirements from volatility3.framework.layers import linear @@ -75,13 +75,13 @@ class Intel(linear.LinearlyMappedLayer): # These can vary depending on the type of space self._index_shift = math.ceil(math.log2(struct.calcsize(self._entry_format))) - @volatility3.classproperty + @classproperty @functools.lru_cache def page_shift(cls) -> int: """Page shift for the intel memory layers.""" return cls._page_size_in_bits - @volatility3.classproperty + @classproperty @functools.lru_cache def page_size(cls) -> int: """Page size for the intel memory layers. @@ -90,30 +90,30 @@ class Intel(linear.LinearlyMappedLayer): """ return 1 << cls._page_size_in_bits - @volatility3.classproperty + @classproperty @functools.lru_cache def page_mask(cls) -> int: """Page mask for the intel memory layers.""" return ~(cls.page_size - 1) - @volatility3.classproperty + @classproperty @functools.lru_cache def bits_per_register(cls) -> int: """Returns the bits_per_register to determine the range of an IntelTranslationLayer.""" return cls._bits_per_register - @volatility3.classproperty + @classproperty @functools.lru_cache def minimum_address(cls) -> int: return 0 - @volatility3.classproperty + @classproperty @functools.lru_cache def maximum_address(cls) -> int: return (1 << cls._maxvirtaddr) - 1 - @volatility3.classproperty + @classproperty def structure(cls) -> List[Tuple[str, int, bool]]: return cls._structure From 14120044227d722d58f62b50552953a9f7771759 Mon Sep 17 00:00:00 2001 From: David McDonald Date: Sat, 29 Mar 2025 15:56:25 -0500 Subject: [PATCH 55/68] Make `registry_layers` -> `registry_layer` for consistency --- .../plugins/windows/registry/lsadump.py | 16 ++++++++-------- .../plugins/windows/registry/userassist.py | 10 +++++----- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/volatility3/framework/plugins/windows/registry/lsadump.py b/volatility3/framework/plugins/windows/registry/lsadump.py index e394822d6..50ecaebc1 100644 --- a/volatility3/framework/plugins/windows/registry/lsadump.py +++ b/volatility3/framework/plugins/windows/registry/lsadump.py @@ -11,7 +11,7 @@ from Crypto.Cipher import ARC4, DES, AES from volatility3.framework import interfaces, renderers, exceptions from volatility3.framework.configuration import requirements -from volatility3.framework.layers import registry as registry_layers +from volatility3.framework.layers import registry as registry_layer from volatility3.framework.symbols.windows import versions from volatility3.plugins.windows.registry import hashdump, hivelist from volatility3.framework.renderers import format_hints @@ -64,7 +64,7 @@ class Lsadump(interfaces.plugins.PluginInterface): @classmethod def get_lsa_key( - cls, sechive: registry_layers.RegistryHive, bootkey: bytes, vista_or_later: bool + cls, sechive: registry_layer.RegistryHive, bootkey: bytes, vista_or_later: bool ) -> Optional[bytes]: if not bootkey: return None @@ -108,7 +108,7 @@ class Lsadump(interfaces.plugins.PluginInterface): @classmethod def get_secret_by_name( cls, - sechive: registry_layers.RegistryHive, + sechive: registry_layer.RegistryHive, name: str, lsakey: bytes, is_vista_or_later: bool, @@ -123,7 +123,7 @@ class Lsadump(interfaces.plugins.PluginInterface): enc_secret_value = next(enc_secret_key.get_values(), None) except ( exceptions.InvalidAddressException, - registry_layers.RegistryException, + registry_layer.RegistryException, ): enc_secret_value = None @@ -171,8 +171,8 @@ class Lsadump(interfaces.plugins.PluginInterface): def _generator( self, - syshive: registry_layers.RegistryHive, - sechive: registry_layers.RegistryHive, + syshive: registry_layer.RegistryHive, + sechive: registry_layer.RegistryHive, ): kernel = self.context.modules[self.config["kernel"]] @@ -208,7 +208,7 @@ class Lsadump(interfaces.plugins.PluginInterface): except ( StopIteration, exceptions.InvalidAddressException, - registry_layers.RegistryException, + registry_layer.RegistryException, ): enc_secret_value = None @@ -231,7 +231,7 @@ class Lsadump(interfaces.plugins.PluginInterface): key_name = key.get_name() except ( exceptions.InvalidAddressException, - registry_layers.RegistryException, + registry_layer.RegistryException, ): key_name = renderers.UnreadableValue() diff --git a/volatility3/framework/plugins/windows/registry/userassist.py b/volatility3/framework/plugins/windows/registry/userassist.py index 3272b241c..d27c8eb0c 100644 --- a/volatility3/framework/plugins/windows/registry/userassist.py +++ b/volatility3/framework/plugins/windows/registry/userassist.py @@ -13,7 +13,7 @@ from typing import Any, Generator, List, Tuple from volatility3.framework import constants, exceptions, interfaces, renderers from volatility3.framework.configuration import requirements from volatility3.framework.layers import physical -from volatility3.framework.layers import registry as registry_layers +from volatility3.framework.layers import registry as registry_layer from volatility3.framework.renderers import conversion, format_hints from volatility3.framework.symbols import intermed from volatility3.plugins.windows.registry import hivelist @@ -155,7 +155,7 @@ class UserAssist(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterfac ).has_member("CookiePad") def list_userassist( - self, hive: registry_layers.RegistryHive + self, hive: registry_layer.RegistryHive ) -> Generator[Tuple[int, Tuple], None, None]: """Generate userassist data for a registry hive.""" @@ -177,7 +177,7 @@ class UserAssist(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterfac "software\\microsoft\\windows\\currentversion\\explorer\\userassist", return_list=True, ) - except registry_layers.RegistryException as e: + except registry_layer.RegistryException as e: vollog.warning( f"Error accessing UserAssist key in {hive_name} at {hive.hive_offset:#x}: {e}" ) @@ -247,7 +247,7 @@ class UserAssist(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterfac subkey_name = subkey.get_name() except ( exceptions.InvalidAddressException, - registry_layers.RegistryException, + registry_layer.RegistryException, ): subkey_name = renderers.UnreadableValue() @@ -276,7 +276,7 @@ class UserAssist(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterfac value_name = value.get_name() except ( exceptions.InvalidAddressException, - registry_layers.RegistryException, + registry_layer.RegistryException, ): value_name = renderers.UnreadableValue() From 524d89ad6ce21c944fcd643beae2cbfc61d1ed29 Mon Sep 17 00:00:00 2001 From: Andrew Case Date: Mon, 31 Mar 2025 23:08:49 +0000 Subject: [PATCH 56/68] Fix several bugs in check_afinfo. Update through latest kernels. Match current Volatility coding standards --- .../framework/plugins/linux/check_afinfo.py | 189 ++++++++++++------ 1 file changed, 131 insertions(+), 58 deletions(-) diff --git a/volatility3/framework/plugins/linux/check_afinfo.py b/volatility3/framework/plugins/linux/check_afinfo.py index 7aa3cbdd2..034d4c24f 100644 --- a/volatility3/framework/plugins/linux/check_afinfo.py +++ b/volatility3/framework/plugins/linux/check_afinfo.py @@ -4,7 +4,7 @@ """A module containing a plugin that verifies the operation function pointers of network protocols.""" import logging -from typing import List +from typing import List, Tuple, Generator from volatility3.framework import exceptions, interfaces from volatility3.framework import renderers @@ -18,6 +18,7 @@ vollog = logging.getLogger(__name__) class Check_afinfo(plugins.PluginInterface): """Verifies the operation function pointers of network protocols.""" + _version = (1, 0, 0) _required_framework_version = (2, 0, 0) @classmethod @@ -30,61 +31,80 @@ class Check_afinfo(plugins.PluginInterface): ), ] - # returns whether the symbol is found within the kernel (system.map) or not - def _is_known_address(self, handler_addr): - symbols = list(self.context.symbol_space.get_symbols_by_location(handler_addr)) + @classmethod + def _check_members( + cls, + context: interfaces.context.ContextInterface, + vmlinux_name: str, + var_ops: interfaces.objects.ObjectInterface, + var_name: str, + members: List[str], + ) -> Generator[Tuple[str, str, int], None, None]: + """ + Yields any members that are not pointing inside the kernel + """ - return len(symbols) > 0 + vmlinux = context.modules[vmlinux_name] - def _check_members(self, var_ops, var_name, members): for check in members: # redhat-specific garbage if check.startswith("__UNIQUE_ID_rh_kabi_hide"): continue - if check == "write": - addr = var_ops.member(attr="write") - else: - addr = getattr(var_ops, check) + # These structures have members like `write` and `next`, which are built in Python functions + addr = var_ops.member(attr=check) - if addr and addr != 0 and not self._is_known_address(addr): - yield check, addr + # Unimplemented handlers are set to 0 + if not addr: + continue - def _check_afinfo(self, var_name, var, op_members, seq_members): - # check if object has a least one of the members used for analysis by this function - required_members = ["seq_fops", "seq_ops", "seq_show"] - has_required_member = any(var.has_member(member) for member in required_members) - if not has_required_member: - vollog.debug( - f"{var_name} object at {hex(var.vol.offset)} had none of the required members: {', '.join([member for member in required_members])}" - ) - raise exceptions.PluginRequirementException + if len(vmlinux.get_symbols_by_absolute_location(addr)) == 0: + yield var_name, check, addr + + @classmethod + def _check_pre_4_18_ops( + cls, + context: interfaces.context.ContextInterface, + vmlinux_name: str, + var_name: str, + var: interfaces.objects.ObjectInterface, + op_members: List[str], + seq_members: List[str], + ): + """ + Finds the correct way to reference `op_members` + """ + vmlinux = context.modules[vmlinux_name] if var.has_member("seq_fops"): - for hooked_member, hook_address in self._check_members( - var.seq_fops, var_name, op_members - ): - yield var_name, hooked_member, hook_address - + yield from cls._check_members( + context, vmlinux_name, var.seq_fops, var_name, op_members + ) # newer kernels if var.has_member("seq_ops"): - for hooked_member, hook_address in self._check_members( - var.seq_ops, var_name, seq_members - ): - yield var_name, hooked_member, hook_address + yield from cls._check_members( + context, vmlinux_name, var.seq_ops, var_name, seq_members + ) # this is the most commonly hooked member by rootkits, so a force a check on it + elif var.has_member("seq_show"): + if len(vmlinux.get_symbols_by_location(var.seq_show)) == 0: + yield var_name, "show", var.seq_show else: - if var.has_member("seq_show"): - if not self._is_known_address(var.seq_show): - yield var_name, "show", var.seq_show - - def _generator(self): - vmlinux = self.context.modules[self.config["kernel"]] - - op_members = vmlinux.get_type("file_operations").members - seq_members = vmlinux.get_type("seq_operations").members + raise exceptions.VolatilityException( + "_check_afinfo_pre_4_18: Unable to find sequence operations members for checking." + ) + @classmethod + def _check_afinfo_pre_4_18( + cls, + context: interfaces.context.ContextInterface, + vmlinux_name: str, + seq_members: str, + ) -> Generator[Tuple[str, str, int], None, None]: + """ + Checks the operations structures for network protocols of < 4.18 systems + """ tcp = ("tcp_seq_afinfo", ["tcp6_seq_afinfo", "tcp4_seq_afinfo"]) udp = ( "udp_seq_afinfo", @@ -97,39 +117,92 @@ class Check_afinfo(plugins.PluginInterface): ) protocols = [tcp, udp] - # used to track the calls to _check_afinfo and the - # number of errors produced due to missing members - symbols_checked = set() - symbols_with_errors = set() + vmlinux = context.modules[vmlinux_name] + + op_members = vmlinux.get_type("file_operations").members # loop through all symbols for struct_type, global_vars in protocols: for global_var_name in global_vars: # this will lookup fail for the IPv6 protocols on kernels without IPv6 support try: - global_var = vmlinux.get_symbol(global_var_name) + global_var = vmlinux.object_from_symbol(global_var_name) except exceptions.SymbolError: continue - global_var = vmlinux.object( - object_type=struct_type, offset=global_var.address + yield from cls._check_pre_4_18_ops( + context, + vmlinux_name, + global_var_name, + global_var, + op_members, + seq_members, ) - symbols_checked.add(global_var_name) - try: - for name, member, address in self._check_afinfo( - global_var_name, global_var, op_members, seq_members - ): - yield 0, (name, member, format_hints.Hex(address)) - except exceptions.PluginRequirementException: - symbols_with_errors.add(global_var_name) + @classmethod + def _check_afinfo_post_4_18( + cls, + context: interfaces.context.ContextInterface, + vmlinux_name: str, + seq_members: str, + ) -> Generator[Tuple[str, str, int], None, None]: + """ + Checks the operations structures for network protocols of >= 4.18 systems + """ + vmlinux = context.modules[vmlinux_name] - # if every call to _check_afinfo failed show a warning - if symbols_checked == symbols_with_errors: - vollog.warning( - "This plugin was not able to check for hooks. This means you are either analyzing an unsupported kernel version or that your symbol table is corrupt." + ops_structs = [ + "raw_seq_ops", + "udp_seq_ops", + "arp_seq_ops", + "unix_seq_ops", + "udp6_seq_ops" "raw6_seq_ops", + "tcp_seq_ops", + "tcp4_seq_ops", + "tcp6_seq_ops", + "packet_seq_ops", + ] + + for protocol_ops_var in ops_structs: + # These will fail if the particular kernel doesn't have support for a protocol like IPv6 + try: + protocol_ops = vmlinux.object_from_symbol(protocol_ops_var) + except exceptions.SymbolError: + continue + + yield from cls._check_members( + context, vmlinux_name, protocol_ops, protocol_ops_var, seq_members ) + @classmethod + def check_afinfo( + cls, context: interfaces.context.ContextInterface, vmlinux_name + ) -> Generator[Tuple[str, str, int], None, None]: + """ + Walks the network protocol operations structures for common network protocols. + Reports any initialized operations members that do not point inside the kernel. + """ + vmlinux = context.modules[vmlinux_name] + + type_check = vmlinux.get_type("tcp_seq_afinfo") + if type_check.has_member("seq_fops"): + checker = cls._check_afinfo_pre_4_18 + else: + checker = cls._check_afinfo_post_4_18 + + seq_members = vmlinux.get_type("seq_operations").members + + yield from checker(context, vmlinux_name, seq_members) + + def _generator(self): + """ + A simple wrapper around `check_afino` + """ + for name, member, address in self.check_afinfo( + self.context, self.config["kernel"] + ): + yield 0, (name, member, format_hints.Hex(address)) + def run(self): return renderers.TreeGrid( [ From 9c58cfc2a844f545626ddcc8e1e8d385c56684bd Mon Sep 17 00:00:00 2001 From: ikelos Date: Tue, 1 Apr 2025 11:20:48 +0100 Subject: [PATCH 57/68] Potential fix for code scanning alert no. 416: Implicit string concatenation in a list Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> --- volatility3/framework/plugins/linux/check_afinfo.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/volatility3/framework/plugins/linux/check_afinfo.py b/volatility3/framework/plugins/linux/check_afinfo.py index 034d4c24f..310e7da6c 100644 --- a/volatility3/framework/plugins/linux/check_afinfo.py +++ b/volatility3/framework/plugins/linux/check_afinfo.py @@ -156,7 +156,7 @@ class Check_afinfo(plugins.PluginInterface): "udp_seq_ops", "arp_seq_ops", "unix_seq_ops", - "udp6_seq_ops" "raw6_seq_ops", + "udp6_seq_ops", "raw6_seq_ops", "tcp_seq_ops", "tcp4_seq_ops", "tcp6_seq_ops", From 2ca5fd5fe584372dbbcdf43759f54b370cdaaf58 Mon Sep 17 00:00:00 2001 From: ikelos Date: Tue, 1 Apr 2025 11:22:55 +0100 Subject: [PATCH 58/68] Update volatility3/framework/plugins/linux/check_afinfo.py Fix up ruff error. --- volatility3/framework/plugins/linux/check_afinfo.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/volatility3/framework/plugins/linux/check_afinfo.py b/volatility3/framework/plugins/linux/check_afinfo.py index 310e7da6c..aa734b25d 100644 --- a/volatility3/framework/plugins/linux/check_afinfo.py +++ b/volatility3/framework/plugins/linux/check_afinfo.py @@ -156,7 +156,8 @@ class Check_afinfo(plugins.PluginInterface): "udp_seq_ops", "arp_seq_ops", "unix_seq_ops", - "udp6_seq_ops", "raw6_seq_ops", + "udp6_seq_ops", + "raw6_seq_ops", "tcp_seq_ops", "tcp4_seq_ops", "tcp6_seq_ops", From caedfc564f150f00c877f0ccfc5fce745217776c Mon Sep 17 00:00:00 2001 From: Andrew Case Date: Tue, 1 Apr 2025 13:34:24 +0000 Subject: [PATCH 59/68] Fix black error --- volatility3/framework/plugins/linux/check_afinfo.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/volatility3/framework/plugins/linux/check_afinfo.py b/volatility3/framework/plugins/linux/check_afinfo.py index aa734b25d..47da21615 100644 --- a/volatility3/framework/plugins/linux/check_afinfo.py +++ b/volatility3/framework/plugins/linux/check_afinfo.py @@ -156,7 +156,7 @@ class Check_afinfo(plugins.PluginInterface): "udp_seq_ops", "arp_seq_ops", "unix_seq_ops", - "udp6_seq_ops", + "udp6_seq_ops", "raw6_seq_ops", "tcp_seq_ops", "tcp4_seq_ops", From 9791ae587898cc7b7324eeaf6d95ca58aae273ae Mon Sep 17 00:00:00 2001 From: Mike Auty Date: Tue, 1 Apr 2025 22:23:03 +0100 Subject: [PATCH 60/68] Fix up direct import issue --- volatility3/cli/text_renderer.py | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/volatility3/cli/text_renderer.py b/volatility3/cli/text_renderer.py index 1d39e3fa6..1453c0ea1 100644 --- a/volatility3/cli/text_renderer.py +++ b/volatility3/cli/text_renderer.py @@ -9,11 +9,10 @@ import random import string import sys from functools import wraps -from typing import Any, Callable, Dict, List, Tuple, TypeVar, Union +from typing import Any, Callable, Dict, List, Optional, Tuple, TypeVar, Union from volatility3.cli import text_filter from volatility3.framework import exceptions, interfaces, renderers -from volatility3.framework.interfaces.renderers import BaseAbsentValue from volatility3.framework.renderers import format_hints vollog = logging.getLogger(__name__) @@ -84,7 +83,9 @@ def multitypedata_as_text(value: format_hints.MultiTypeData) -> str: T = TypeVar("T") -def optional(func: Callable[[Union[BaseAbsentValue, T]], str]) -> Callable[[T], str]: +def optional( + func: Callable[[Union[interfaces.renderers.BaseAbsentValue, T]], str], +) -> Callable[[T], str]: @wraps(func) def wrapped(x: Any) -> str: if isinstance(x, interfaces.renderers.BaseAbsentValue): @@ -156,8 +157,10 @@ class LayerDataRenderer(CLITypeRenderer): self.display_hex = True self.display_ascii = True - def render(data: Union[renderers.LayerData, BaseAbsentValue]): - if isinstance(data, BaseAbsentValue): + def render( + data: Union[renderers.LayerData, interfaces.renderers.BaseAbsentValue], + ): + if isinstance(data, interfaces.renderers.BaseAbsentValue): # FIXME: Do something cleverer here return "" @@ -241,8 +244,8 @@ class CLIRenderer(interfaces.renderers.Renderer): name = "unnamed" structured_output = False - filter: text_filter.CLIFilter = None - column_hide_list: list = None + filter: Optional[text_filter.CLIFilter] = None + column_hide_list: Optional[list] = None def ignored_columns( self, From e2fb96a0d338109ddb9d2431ea7fb18ed8b20041 Mon Sep 17 00:00:00 2001 From: Mike Auty Date: Tue, 1 Apr 2025 23:14:03 +0100 Subject: [PATCH 61/68] Fix up JSON rendering of hex bytes and LayerData --- volatility3/cli/text_renderer.py | 77 ++++++++++++++++++-------------- 1 file changed, 44 insertions(+), 33 deletions(-) diff --git a/volatility3/cli/text_renderer.py b/volatility3/cli/text_renderer.py index 1453c0ea1..39abb265e 100644 --- a/volatility3/cli/text_renderer.py +++ b/volatility3/cli/text_renderer.py @@ -159,42 +159,12 @@ class LayerDataRenderer(CLITypeRenderer): def render( data: Union[renderers.LayerData, interfaces.renderers.BaseAbsentValue], - ): + ) -> str: if isinstance(data, interfaces.renderers.BaseAbsentValue): # FIXME: Do something cleverer here return "" - context_byte_len = self.context_byte_len if not data.no_surrounding else 0 - - layer = data.context.layers[data.layer_name] - # Map of the holes - error_bytes = set() - start_offset = data.offset - context_byte_len - end_offset = data.offset + data.length + context_byte_len - if isinstance(layer, interfaces.layers.TranslationLayerInterface): - error_bytes = set() - mapping = iter(layer.mapping(start_offset, end_offset, True)) - current_map = next(mapping) - for i in range(start_offset, end_offset): - # Run through the bytes, check if they're present - offset, sublength, _, _, _ = current_map - if i < offset: - error_bytes.add(i - start_offset) - if i > offset + sublength: - try: - current_map = next(mapping) - except StopIteration: - pass - offset, sublength, _, _, _ = current_map - if i > offset + sublength: - error_bytes.add(i - start_offset) - - # Padded data - specific_data = data.context.layers[data.layer_name].read( - start_offset, - end_offset - start_offset, - True, - ) + specific_data, error_bytes = self.render_bytes(data) printables = "" output = "\n" @@ -224,6 +194,46 @@ class LayerDataRenderer(CLITypeRenderer): render_func = render return super().__init__(render_func) + def render_bytes(self, data: renderers.LayerData) -> tuple[bytes, set[int]]: + """Renders a valid LayerData into bytes (with context bytes)""" + context_byte_len = self.context_byte_len if not data.no_surrounding else 0 + + layer = data.context.layers[data.layer_name] + # Map of the holes + error_bytes = set() + start_offset = data.offset - context_byte_len + end_offset = data.offset + data.length + context_byte_len + if isinstance(layer, interfaces.layers.TranslationLayerInterface): + error_bytes = set() + mapping = iter(layer.mapping(start_offset, end_offset, True)) + current_map = next(mapping) + for i in range(start_offset, end_offset): + # Run through the bytes, check if they're present + offset, sublength, _, _, _ = current_map + if i < offset: + error_bytes.add(i - start_offset) + if i > offset + sublength: + try: + current_map = next(mapping) + except StopIteration: + pass + offset, sublength, _, _, _ = current_map + if i > offset + sublength: + error_bytes.add(i - start_offset) + + # Padded data + specific_data = data.context.layers[data.layer_name].read( + start_offset, + end_offset - start_offset, + True, + ) + + import pdb + + pdb.set_trace() + + return specific_data, error_bytes + class CLIRenderer(interfaces.renderers.Renderer): """Class to add specific requirements for CLI renderers.""" @@ -525,9 +535,10 @@ class PrettyTextRenderer(CLIRenderer): class JsonRenderer(CLIRenderer): _type_renderers = { - format_hints.HexBytes: quoted_optional(hex_bytes_as_text), + format_hints.HexBytes: lambda x: x.hex(" "), renderers.Disassembly: quoted_optional(display_disassembly), format_hints.MultiTypeData: quoted_optional(multitypedata_as_text), + renderers.LayerData: lambda x: LayerDataRenderer().render_bytes(x)[0].hex(" "), bytes: optional(lambda x: " ".join(f"{b:02x}" for b in x)), datetime.datetime: lambda x: ( x.isoformat() From aad6a563364fc36f582751f27fc72c041cb364b6 Mon Sep 17 00:00:00 2001 From: Mike Auty Date: Tue, 1 Apr 2025 23:20:49 +0100 Subject: [PATCH 62/68] Fix old typing mechanism --- volatility3/cli/text_renderer.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/volatility3/cli/text_renderer.py b/volatility3/cli/text_renderer.py index 39abb265e..06c6564de 100644 --- a/volatility3/cli/text_renderer.py +++ b/volatility3/cli/text_renderer.py @@ -9,7 +9,7 @@ import random import string import sys from functools import wraps -from typing import Any, Callable, Dict, List, Optional, Tuple, TypeVar, Union +from typing import Any, Callable, Dict, List, Optional, Set, Tuple, TypeVar, Union from volatility3.cli import text_filter from volatility3.framework import exceptions, interfaces, renderers @@ -194,7 +194,7 @@ class LayerDataRenderer(CLITypeRenderer): render_func = render return super().__init__(render_func) - def render_bytes(self, data: renderers.LayerData) -> tuple[bytes, set[int]]: + def render_bytes(self, data: renderers.LayerData) -> Tuple[bytes, Set[int]]: """Renders a valid LayerData into bytes (with context bytes)""" context_byte_len = self.context_byte_len if not data.no_surrounding else 0 From d3d19fe776782f2a82b39ea3bbe38082e61b4169 Mon Sep 17 00:00:00 2001 From: Mike Auty Date: Tue, 1 Apr 2025 23:37:57 +0100 Subject: [PATCH 63/68] CLI: Handle BaseAbsentValues in JSON --- volatility3/cli/text_renderer.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/volatility3/cli/text_renderer.py b/volatility3/cli/text_renderer.py index 06c6564de..69a5468a7 100644 --- a/volatility3/cli/text_renderer.py +++ b/volatility3/cli/text_renderer.py @@ -535,10 +535,18 @@ class PrettyTextRenderer(CLIRenderer): class JsonRenderer(CLIRenderer): _type_renderers = { - format_hints.HexBytes: lambda x: x.hex(" "), + format_hints.HexBytes: lambda x: ( + x.hex(" ") + if not isinstance(x, interfaces.renderers.BaseAbsentValue) + else "N/A" + ), renderers.Disassembly: quoted_optional(display_disassembly), format_hints.MultiTypeData: quoted_optional(multitypedata_as_text), - renderers.LayerData: lambda x: LayerDataRenderer().render_bytes(x)[0].hex(" "), + renderers.LayerData: lambda x: ( + LayerDataRenderer().render_bytes(x)[0].hex(" ") + if not isinstance(x, interfaces.renderers.BaseAbsentValue) + else "N/A" + ), bytes: optional(lambda x: " ".join(f"{b:02x}" for b in x)), datetime.datetime: lambda x: ( x.isoformat() From 0b1bbb87eee2d0a700d7601170e92ff630cf2782 Mon Sep 17 00:00:00 2001 From: David McDonald Date: Wed, 2 Apr 2025 09:36:07 -0500 Subject: [PATCH 64/68] Windows Tests: Update userassist JSON output The new layer data type renders the output a little differently, and the plugin also seems to render 'N/A' for a missing value where previously it was an empty string. --- ...windows.registry.userassist.UserAssist.json | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/test/plugins/windows/test_data/windows.registry.userassist.UserAssist.json b/test/plugins/windows/test_data/windows.registry.userassist.UserAssist.json index 6ae740822..fd1c997b0 100644 --- a/test/plugins/windows/test_data/windows.registry.userassist.UserAssist.json +++ b/test/plugins/windows/test_data/windows.registry.userassist.UserAssist.json @@ -9,7 +9,7 @@ "Last Write Time": "2025-03-06T17:57:09+00:00", "Name": null, "Path": "ntuser.dat\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Explorer\\UserAssist\\{F4E57C4B-2036-45F0-A9AB-443BCFE33D9F}\\Count", - "Raw Data": "", + "Raw Data": "N/A", "Time Focused": null, "Type": "Key", "__children": [ @@ -23,7 +23,7 @@ "Last Write Time": "2025-03-06T17:57:09+00:00", "Name": "%ALLUSERSPROFILE%\\Microsoft\\Windows\\Start Menu\\Programs\\Accessories\\Paint.lnk", "Path": "ntuser.dat\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Explorer\\UserAssist\\{F4E57C4B-2036-45F0-A9AB-443BCFE33D9F}\\Count", - "Raw Data": "\"\n00 00 00 00 07 00 00 00 00 00 00 00 07 00 00 00 ................\n00 00 80 bf 00 00 80 bf 00 00 80 bf 00 00 80 bf ................\n00 00 80 bf 00 00 80 bf 00 00 80 bf 00 00 80 bf ................\n00 00 80 bf 00 00 80 bf ff ff ff ff 90 86 6b 31 ..............k1\nfd 8d db 01 00 00 00 00 ........ \"", + "Raw Data": "00 00 00 00 07 00 00 00 00 00 00 00 07 00 00 00 00 00 80 bf 00 00 80 bf 00 00 80 bf 00 00 80 bf 00 00 80 bf 00 00 80 bf 00 00 80 bf 00 00 80 bf 00 00 80 bf 00 00 80 bf ff ff ff ff 90 86 6b 31 fd 8d db 01 00 00 00 00", "Time Focused": "0:00:00.507000", "Type": "Value", "__children": [] @@ -38,7 +38,7 @@ "Last Write Time": "2025-03-06T17:57:09+00:00", "Name": "%ALLUSERSPROFILE%\\Microsoft\\Windows\\Start Menu\\Programs\\Administrative Tools\\Registry Editor.lnk", "Path": "ntuser.dat\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Explorer\\UserAssist\\{F4E57C4B-2036-45F0-A9AB-443BCFE33D9F}\\Count", - "Raw Data": "\"\n00 00 00 00 01 00 00 00 00 00 00 00 01 00 00 00 ................\n00 00 80 bf 00 00 80 bf 00 00 80 bf 00 00 80 bf ................\n00 00 80 bf 00 00 80 bf 00 00 80 bf 00 00 80 bf ................\n00 00 80 bf 00 00 80 bf ff ff ff ff f0 82 cf ca ................\n95 8e db 01 00 00 00 00 ........ \"", + "Raw Data": "00 00 00 00 01 00 00 00 00 00 00 00 01 00 00 00 00 00 80 bf 00 00 80 bf 00 00 80 bf 00 00 80 bf 00 00 80 bf 00 00 80 bf 00 00 80 bf 00 00 80 bf 00 00 80 bf 00 00 80 bf ff ff ff ff f0 82 cf ca 95 8e db 01 00 00 00 00", "Time Focused": "0:00:00.501000", "Type": "Value", "__children": [] @@ -53,7 +53,7 @@ "Last Write Time": "2025-03-06T17:57:09+00:00", "Name": "%APPDATA%\\Microsoft\\Windows\\Start Menu\\Programs\\Windows PowerShell\\Windows PowerShell.lnk", "Path": "ntuser.dat\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Explorer\\UserAssist\\{F4E57C4B-2036-45F0-A9AB-443BCFE33D9F}\\Count", - "Raw Data": "\"\n00 00 00 00 04 00 00 00 00 00 00 00 04 00 00 00 ................\n00 00 80 bf 00 00 80 bf 00 00 80 bf 00 00 80 bf ................\n00 00 80 bf 00 00 80 bf 00 00 80 bf 00 00 80 bf ................\n00 00 80 bf 00 00 80 bf ff ff ff ff 10 67 cf 4d .............g.M\nbe 8e db 01 00 00 00 00 ........ \"", + "Raw Data": "00 00 00 00 04 00 00 00 00 00 00 00 04 00 00 00 00 00 80 bf 00 00 80 bf 00 00 80 bf 00 00 80 bf 00 00 80 bf 00 00 80 bf 00 00 80 bf 00 00 80 bf 00 00 80 bf 00 00 80 bf ff ff ff ff 10 67 cf 4d be 8e db 01 00 00 00 00", "Time Focused": "0:00:00.504000", "Type": "Value", "__children": [] @@ -68,7 +68,7 @@ "Last Write Time": "2025-03-06T17:57:09+00:00", "Name": "%APPDATA%\\Microsoft\\Windows\\Start Menu\\Programs\\System Tools\\Command Prompt.lnk", "Path": "ntuser.dat\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Explorer\\UserAssist\\{F4E57C4B-2036-45F0-A9AB-443BCFE33D9F}\\Count", - "Raw Data": "\"\n00 00 00 00 01 00 00 00 00 00 00 00 01 00 00 00 ................\n00 00 80 bf 00 00 80 bf 00 00 80 bf 00 00 80 bf ................\n00 00 80 bf 00 00 80 bf 00 00 80 bf 00 00 80 bf ................\n00 00 80 bf 00 00 80 bf ff ff ff ff d0 99 66 6c ..............fl\nc0 8e db 01 00 00 00 00 ........ \"", + "Raw Data": "00 00 00 00 01 00 00 00 00 00 00 00 01 00 00 00 00 00 80 bf 00 00 80 bf 00 00 80 bf 00 00 80 bf 00 00 80 bf 00 00 80 bf 00 00 80 bf 00 00 80 bf 00 00 80 bf 00 00 80 bf ff ff ff ff d0 99 66 6c c0 8e db 01 00 00 00 00", "Time Focused": "0:00:00.501000", "Type": "Value", "__children": [] @@ -83,7 +83,7 @@ "Last Write Time": "2025-03-06T17:57:09+00:00", "Name": "%ALLUSERSPROFILE%\\Microsoft\\Windows\\Start Menu\\Programs\\Accessories\\Notepad.lnk", "Path": "ntuser.dat\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Explorer\\UserAssist\\{F4E57C4B-2036-45F0-A9AB-443BCFE33D9F}\\Count", - "Raw Data": "\"\n00 00 00 00 01 00 00 00 00 00 00 00 01 00 00 00 ................\n00 00 80 bf 00 00 80 bf 00 00 80 bf 00 00 80 bf ................\n00 00 80 bf 00 00 80 bf 00 00 80 bf 00 00 80 bf ................\n00 00 80 bf 00 00 80 bf ff ff ff ff 00 62 ba 89 .............b..\nc0 8e db 01 00 00 00 00 ........ \"", + "Raw Data": "00 00 00 00 01 00 00 00 00 00 00 00 01 00 00 00 00 00 80 bf 00 00 80 bf 00 00 80 bf 00 00 80 bf 00 00 80 bf 00 00 80 bf 00 00 80 bf 00 00 80 bf 00 00 80 bf 00 00 80 bf ff ff ff ff 00 62 ba 89 c0 8e db 01 00 00 00 00", "Time Focused": "0:00:00.501000", "Type": "Value", "__children": [] @@ -98,7 +98,7 @@ "Last Write Time": "2025-03-06T17:57:09+00:00", "Name": "%ALLUSERSPROFILE%\\Microsoft\\Windows\\Start Menu\\Programs\\Administrative Tools\\Task Scheduler.lnk", "Path": "ntuser.dat\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Explorer\\UserAssist\\{F4E57C4B-2036-45F0-A9AB-443BCFE33D9F}\\Count", - "Raw Data": "\"\n00 00 00 00 02 00 00 00 00 00 00 00 02 00 00 00 ................\n00 00 80 bf 00 00 80 bf 00 00 80 bf 00 00 80 bf ................\n00 00 80 bf 00 00 80 bf 00 00 80 bf 00 00 80 bf ................\n00 00 80 bf 00 00 80 bf ff ff ff ff b0 24 49 23 .............$I#\nc1 8e db 01 00 00 00 00 ........ \"", + "Raw Data": "00 00 00 00 02 00 00 00 00 00 00 00 02 00 00 00 00 00 80 bf 00 00 80 bf 00 00 80 bf 00 00 80 bf 00 00 80 bf 00 00 80 bf 00 00 80 bf 00 00 80 bf 00 00 80 bf 00 00 80 bf ff ff ff ff b0 24 49 23 c1 8e db 01 00 00 00 00", "Time Focused": "0:00:00.502000", "Type": "Value", "__children": [] @@ -113,11 +113,11 @@ "Last Write Time": "2025-03-06T17:57:09+00:00", "Name": "%ALLUSERSPROFILE%\\Microsoft\\Windows\\Start Menu\\Programs\\Microsoft Edge.lnk", "Path": "ntuser.dat\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Explorer\\UserAssist\\{F4E57C4B-2036-45F0-A9AB-443BCFE33D9F}\\Count", - "Raw Data": "\"\n00 00 00 00 01 00 00 00 00 00 00 00 01 00 00 00 ................\n00 00 80 bf 00 00 80 bf 00 00 80 bf 00 00 80 bf ................\n00 00 80 bf 00 00 80 bf 00 00 80 bf 00 00 80 bf ................\n00 00 80 bf 00 00 80 bf ff ff ff ff 60 3d 89 2e ............`=..\nc1 8e db 01 00 00 00 00 ........ \"", + "Raw Data": "00 00 00 00 01 00 00 00 00 00 00 00 01 00 00 00 00 00 80 bf 00 00 80 bf 00 00 80 bf 00 00 80 bf 00 00 80 bf 00 00 80 bf 00 00 80 bf 00 00 80 bf 00 00 80 bf 00 00 80 bf ff ff ff ff 60 3d 89 2e c1 8e db 01 00 00 00 00", "Time Focused": "0:00:00.501000", "Type": "Value", "__children": [] } ] } -} \ No newline at end of file +} From e446c1081de5a54afe2a02f6bea581d6ec9880f8 Mon Sep 17 00:00:00 2001 From: David McDonald Date: Wed, 2 Apr 2025 09:43:20 -0500 Subject: [PATCH 65/68] Remove debugging call --- volatility3/cli/text_renderer.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/volatility3/cli/text_renderer.py b/volatility3/cli/text_renderer.py index 69a5468a7..3fd804d1b 100644 --- a/volatility3/cli/text_renderer.py +++ b/volatility3/cli/text_renderer.py @@ -228,10 +228,6 @@ class LayerDataRenderer(CLITypeRenderer): True, ) - import pdb - - pdb.set_trace() - return specific_data, error_bytes From 5befbf86298cbff014996e4b59c58f850aafb388 Mon Sep 17 00:00:00 2001 From: David McDonald Date: Wed, 2 Apr 2025 14:08:18 -0500 Subject: [PATCH 66/68] Tests: Fix MFTScan testdata These test values needed updating now that the `LayerData` type is used and presents the data a little differently than before. --- test/plugins/windows/windows.py | 37 ++++++++++++++++++--------------- 1 file changed, 20 insertions(+), 17 deletions(-) diff --git a/test/plugins/windows/windows.py b/test/plugins/windows/windows.py index c9cf93391..6733d543e 100644 --- a/test/plugins/windows/windows.py +++ b/test/plugins/windows/windows.py @@ -1,10 +1,10 @@ -import json -import hashlib -import shutil import contextlib -import tempfile +import hashlib +import json import os -from test import test_volatility, WindowsSamples +import shutil +import tempfile +from test import WindowsSamples, test_volatility class TestWindowsVolshell: @@ -843,20 +843,22 @@ class TestWindowsMFTscan: { "ADS Filename": "Zone.Identifier", "Filename": "libby_hoeler_part1.wmv", - "Hexdump": '"\n5b 5a 6f 6e 65 54 72 61 6e 73 66 65 72 5d 0d 0a [ZoneTransfer]..\n5a 6f 6e 65 49 64 3d 33 0d 0a ZoneId=3.. "', + "Hexdump": "5b 5a 6f 6e 65 54 72 61 6e 73 66 65 72 5d 0d 0a 5a 6f 6e 65 49 64 3d 33 0d 0a", "MFT Type": "DATA", "Offset": 55926304, "Record Number": 323, "Record Type": "FILE", + "__children": [], }, { "ADS Filename": "Zone.Identifier", "Filename": "NetZeroQuickHelpLite.exe", - "Hexdump": '"\n5b 5a 6f 6e 65 54 72 61 6e 73 66 65 72 5d 0d 0a [ZoneTransfer]..\n5a 6f 6e 65 49 64 3d 33 0d 0a ZoneId=3.. "', + "Hexdump": "5b 5a 6f 6e 65 54 72 61 6e 73 66 65 72 5d 0d 0a 5a 6f 6e 65 49 64 3d 33 0d 0a", "MFT Type": "DATA", "Offset": 56102400, "Record Number": 347, "Record Type": "FILE", + "__children": [], }, ] for expected_row in expected_rows: @@ -877,20 +879,22 @@ class TestWindowsMFTscan: { "ADS Filename": "$Max", "Filename": "$UsnJrnl", - "Hexdump": '"\n00 00 00 02 00 00 00 00 00 00 80 00 00 00 00 00 ................\nb9 dd f0 cc df 73 db 01 00 00 00 00 00 00 00 00 .....s.........."', + "Hexdump": "00 00 00 02 00 00 00 00 00 00 80 00 00 00 00 00 b9 dd f0 cc df 73 db 01 00 00 00 00 00 00 00 00", "MFT Type": "DATA", - "Offset": 1058018088, + "Offset": 26235616, "Record Number": 107240, "Record Type": "FILE", + "__children": [], }, { - "ADS Filename": "$Config", - "Filename": "$Repair", - "Hexdump": '"\n01 00 00 00 03 00 00 00 ........ "', + "ADS Filename": "$SRAT", + "Filename": "$Bitmap", + "Hexdump": "a4 5f fd 60 38 00 01 03 10 00 0c 00 04 00 00 00 01 00 00 00 01 00 00 00 8d 4e 16 00 02 00 00 00 a0 00 00 00 00 00 06 00 03 00 00 00 01 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 4a 7b 01 00 00 00 00 00", "MFT Type": "DATA", - "Offset": 5009678688, - "Record Number": 28, + "Offset": 1052277088, + "Record Number": 6, "Record Type": "FILE", + "__children": [], }, ] for expected_row in expected_rows: @@ -924,7 +928,7 @@ class TestWindowsMFTscan: expected_rows = [ { "Filename": "index", - "Hexdump": '"\n30 5c 72 a7 1b 6d fb fc 09 00 00 00 00 00 00 00 0\\r..m..........\n00 00 00 00 00 00 00 00 ........ "', + "Hexdump": "30 5c 72 a7 1b 6d fb fc 09 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00", "MFT Type": "DATA", "Offset": 4961536280, "Record Number": 116474, @@ -932,7 +936,7 @@ class TestWindowsMFTscan: }, { "Filename": "0.2.filtertrie.intermediate.txt", - "Hexdump": '"\n30 09 32 0d 0a 0.2.. "', + "Hexdump": "30 09 32 0d 0a", "MFT Type": "DATA", "Offset": 619242944, "Record Number": 113013, @@ -1411,4 +1415,3 @@ class TestWindowsVirtMap: ) for expected_row in expected_rows: assert test_volatility.match_output_row(expected_row, json_out) - From b01c17f419266a71a3554d82e9d4aaa280785e9a Mon Sep 17 00:00:00 2001 From: Mike Auty Date: Thu, 3 Apr 2025 16:24:44 +0100 Subject: [PATCH 67/68] CLI: Fix bad typing issue in pretty printer Fixes #1759 --- volatility3/cli/text_renderer.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/volatility3/cli/text_renderer.py b/volatility3/cli/text_renderer.py index 3fd804d1b..044f33ed1 100644 --- a/volatility3/cli/text_renderer.py +++ b/volatility3/cli/text_renderer.py @@ -466,7 +466,9 @@ class PrettyTextRenderer(CLIRenderer): accumulator.append((node.path_depth, line)) return accumulator - final_output: List[Tuple[int, Dict[interfaces.renderers.Column, str]]] = [] + final_output: List[Tuple[int, Dict[interfaces.renderers.Column, list[str]]]] = ( + [] + ) if not grid.populated: grid.populate(visitor, final_output) else: @@ -503,7 +505,9 @@ class PrettyTextRenderer(CLIRenderer): if column in ignore_columns: del line[column] else: - line[column] = line[column] + ("" * (nums_line - len(line[column]))) + line[column] = line[column] + ( + [""] * (nums_line - len(line[column])) + ) for index in range(nums_line): if index == 0: outfd.write( From ed1b1f5369b75efb9a0a581c6757efce60efb7d6 Mon Sep 17 00:00:00 2001 From: Mike Auty Date: Wed, 2 Apr 2025 21:10:06 +0100 Subject: [PATCH 68/68] Avoid bumping the version too quickly without reason --- volatility3/framework/constants/_version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/volatility3/framework/constants/_version.py b/volatility3/framework/constants/_version.py index 64707b782..a299f15a2 100644 --- a/volatility3/framework/constants/_version.py +++ b/volatility3/framework/constants/_version.py @@ -1,7 +1,7 @@ # We use the SemVer 2.0.0 versioning scheme VERSION_MAJOR = 2 # Number of releases of the library with a breaking change VERSION_MINOR = 26 # Number of changes that only add to the interface -VERSION_PATCH = 0 # Number of changes that do not change the interface +VERSION_PATCH = 1 # Number of changes that do not change the interface VERSION_SUFFIX = "" PACKAGE_VERSION = (