From c18c6cbf3038454f9a67fe243af23702cfa28d49 Mon Sep 17 00:00:00 2001 From: Mike Auty Date: Sun, 16 Feb 2025 16:39:53 +0000 Subject: [PATCH] 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."""