From 55151f546d0e1ccc65c034075eaaaba324cf4734 Mon Sep 17 00:00:00 2001 From: Mike Auty Date: Fri, 22 Nov 2024 11:46:04 +0000 Subject: [PATCH 01/20] 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/20] 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/20] 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/20] 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/20] 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/20] 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/20] 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/20] 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/20] 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/20] 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/20] 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/20] 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/20] 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/20] 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/20] 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/20] 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/20] 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/20] 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/20] 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/20] 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(), )