From 4d19181848d4728f3e94bc03b200721c9399e62d Mon Sep 17 00:00:00 2001 From: Dave Lassalle Date: Thu, 17 Apr 2025 16:42:17 -0500 Subject: [PATCH 01/86] #1780 - add LoadCount to dlllist output --- volatility3/framework/plugins/windows/dlllist.py | 6 ++++++ volatility3/framework/symbols/windows/__init__.py | 1 + .../symbols/windows/extensions/__init__.py | 14 ++++++++++++++ 3 files changed, 21 insertions(+) diff --git a/volatility3/framework/plugins/windows/dlllist.py b/volatility3/framework/plugins/windows/dlllist.py index b851cf7fd..1e8ecd414 100644 --- a/volatility3/framework/plugins/windows/dlllist.py +++ b/volatility3/framework/plugins/windows/dlllist.py @@ -173,6 +173,10 @@ class DllList(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface): except exceptions.InvalidAddressException: size_of_image = renderers.NotAvailableValue() + LoadCount = entry.get_load_count() + if LoadCount is None: + LoadCount = renderers.NotAvailableValue() + yield ( 0, ( @@ -186,6 +190,7 @@ class DllList(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface): size_of_image, BaseDllName, FullDllName, + LoadCount, DllLoadTime, file_output, ), @@ -232,6 +237,7 @@ class DllList(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface): ("Size", format_hints.Hex), ("Name", str), ("Path", str), + ("LoadCount", int), ("LoadTime", datetime.datetime), ("File output", str), ], diff --git a/volatility3/framework/symbols/windows/__init__.py b/volatility3/framework/symbols/windows/__init__.py index f9541579e..3296d7d2c 100755 --- a/volatility3/framework/symbols/windows/__init__.py +++ b/volatility3/framework/symbols/windows/__init__.py @@ -41,6 +41,7 @@ class WindowsKernelIntermedSymbols(intermed.IntermediateSymbolTable): self.set_type_class("_POOL_TRACKER_BIG_PAGES", pool.POOL_TRACKER_BIG_PAGES) self.set_type_class("_IMAGE_DOS_HEADER", pe.IMAGE_DOS_HEADER) self.set_type_class("_KTIMER", extensions.KTIMER) + self.set_type_class("_LDR_DATA_TABLE_ENTRY", extensions.LDR_DATA_TABLE_ENTRY) # Might not necessarily defined in every version of windows self.optional_set_type_class("_IMAGE_NT_HEADERS", pe.IMAGE_NT_HEADERS) diff --git a/volatility3/framework/symbols/windows/extensions/__init__.py b/volatility3/framework/symbols/windows/extensions/__init__.py index 4fc65564e..a814fd12c 100755 --- a/volatility3/framework/symbols/windows/extensions/__init__.py +++ b/volatility3/framework/symbols/windows/extensions/__init__.py @@ -1710,3 +1710,17 @@ class SHARED_CACHE_MAP(objects.StructType): ) return vacb_list + +class LDR_DATA_TABLE_ENTRY(objects.StructType): + def get_load_count(self) -> Optional[int]: + try: + LoadCount = self.LoadCount + except: + try: + LoadCount = self.ObsoleteLoadCount + except: + LoadCount = None + if LoadCount == 65535: + LoadCount = -1 + + return LoadCount From 094ba8e269d5a212e13bae4e7642da86cffec0a6 Mon Sep 17 00:00:00 2001 From: Dave Lassalle Date: Thu, 17 Apr 2025 16:44:22 -0500 Subject: [PATCH 02/86] #1780 - black and ruff fixes --- volatility3/framework/symbols/windows/extensions/__init__.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/volatility3/framework/symbols/windows/extensions/__init__.py b/volatility3/framework/symbols/windows/extensions/__init__.py index a814fd12c..61138a78c 100755 --- a/volatility3/framework/symbols/windows/extensions/__init__.py +++ b/volatility3/framework/symbols/windows/extensions/__init__.py @@ -1711,14 +1711,15 @@ class SHARED_CACHE_MAP(objects.StructType): return vacb_list + class LDR_DATA_TABLE_ENTRY(objects.StructType): def get_load_count(self) -> Optional[int]: try: LoadCount = self.LoadCount - except: + except Exception: try: LoadCount = self.ObsoleteLoadCount - except: + except Exception: LoadCount = None if LoadCount == 65535: LoadCount = -1 From 7e77ee0e24cf5d95845a4f3db6a8c6854a9f6a36 Mon Sep 17 00:00:00 2001 From: tvanegro Date: Tue, 17 Jun 2025 10:24:04 +0200 Subject: [PATCH 03/86] Adding dump dirty page feature --- .../plugins/linux/malware/malfind.py | 22 ++++++++++++-- .../symbols/linux/extensions/__init__.py | 29 ++++++++++++++++++- 2 files changed, 48 insertions(+), 3 deletions(-) diff --git a/volatility3/framework/plugins/linux/malware/malfind.py b/volatility3/framework/plugins/linux/malware/malfind.py index c7e141c02..a8104fbaa 100644 --- a/volatility3/framework/plugins/linux/malware/malfind.py +++ b/volatility3/framework/plugins/linux/malware/malfind.py @@ -37,6 +37,16 @@ class Malfind(interfaces.plugins.PluginInterface): element_type=int, optional=True, ), + requirements.IntRequirement( + name="dumpsize", + description="Dump X bytes of each malicious region found", + optional=True, + ), + requirements.BooleanRequirement( + name="dumppage", + description="Dump dirty page content (for each dirty page)", + optional=True, + ), ] def _list_injections( @@ -50,6 +60,8 @@ class Malfind(interfaces.plugins.PluginInterface): return None proc_layer = self.context.layers[proc_layer_name] + dumpsize = self.config.get("dumpsize") if self.config.get("dumpsize") is not None else 64 + dumppage = self.config.get("dumppage") or False for vma in task.mm.get_vma_iter(): vma_name = vma.get_name(self.context, task) @@ -57,8 +69,14 @@ class Malfind(interfaces.plugins.PluginInterface): f"Injections : processing PID {task.pid} : VMA {vma_name} : {hex(vma.vm_start)}-{hex(vma.vm_end)}" ) if vma.is_suspicious(proc_layer) and vma_name != "[vdso]": - data = proc_layer.read(vma.vm_start, 64, pad=True) - yield vma, vma_name, data + malicious_pages = vma.get_malicious_pages(proc_layer) + if dumppage: + for page_addr in malicious_pages: + data = proc_layer.read(page_addr, dumpsize, pad=True) + yield vma, vma_name+f", page address: {page_addr:#x}, offset: {page_addr-vma.vm_start:#x}", data + else: + data = proc_layer.read(vma.vm_start,dumpsize,pad=True) + yield vma, vma_name, data def _generator(self, tasks): # determine if we're on a 32 or 64 bit kernel diff --git a/volatility3/framework/symbols/linux/extensions/__init__.py b/volatility3/framework/symbols/linux/extensions/__init__.py index 3b9a73e7c..27cb44989 100644 --- a/volatility3/framework/symbols/linux/extensions/__init__.py +++ b/volatility3/framework/symbols/linux/extensions/__init__.py @@ -1273,6 +1273,32 @@ class vm_area_struct(objects.StructType): except exceptions.InvalidAddressException: return None + def get_malicious_pages(self,proclayer=None): + malicious_pages = [] + + flags_str = self.get_protection() + + if flags_str == "rwx": + ret = True + elif flags_str == "r-x" and self.vm_file.dereference().vol.offset == 0: + ret = True + elif proclayer and "x" in flags_str: + for i in range(self.vm_start, self.vm_end, proclayer.page_size): + try: + if proclayer.is_dirty(i): + vollog.debug( + f"Found malicious (dirty+exec) page at {hex(i)} !" + ) + malicious_pages.append(i) + except ( + exceptions.PagedInvalidAddressException, + exceptions.InvalidAddressException, + ) as excp: + vollog.debug(f"Unable to translate address {hex(i)} : {excp}") + # Abort as it is likely that other addresses in the same range will also fail + break + return malicious_pages + # used by malfind def is_suspicious(self, proclayer=None): ret = False @@ -1288,7 +1314,7 @@ class vm_area_struct(objects.StructType): try: if proclayer.is_dirty(i): vollog.warning( - f"Found malicious (dirty+exec) page at {hex(i)} !" + f"Found malicious page(s) inside (dirty+exec) region {hex(self.vm_start)} !" ) # We do not attempt to find other dirty+exec pages once we have found one ret = True @@ -2733,6 +2759,7 @@ class page(objects.StructType): for name, value in self.pageflags_enum.items(): if self.flags & (1 << value) != 0: flags.append(name) + print(name,value) return flags From 96fe242c9d0cbe35e0c653c18c2700e4bed441d2 Mon Sep 17 00:00:00 2001 From: tvanegro Date: Tue, 17 Jun 2025 11:00:18 +0200 Subject: [PATCH 04/86] Minor cleanup + comments --- .../plugins/linux/malware/malfind.py | 33 +++++++++++++------ .../symbols/linux/extensions/__init__.py | 12 +++---- 2 files changed, 28 insertions(+), 17 deletions(-) diff --git a/volatility3/framework/plugins/linux/malware/malfind.py b/volatility3/framework/plugins/linux/malware/malfind.py index a8104fbaa..39d885f68 100644 --- a/volatility3/framework/plugins/linux/malware/malfind.py +++ b/volatility3/framework/plugins/linux/malware/malfind.py @@ -38,13 +38,13 @@ class Malfind(interfaces.plugins.PluginInterface): optional=True, ), requirements.IntRequirement( - name="dumpsize", - description="Dump X bytes of each malicious region found", + name="dump_size", + description="Amount of bytes to dump for each dirty region/page found - Default 64 bytes", optional=True, ), requirements.BooleanRequirement( - name="dumppage", - description="Dump dirty page content (for each dirty page)", + name="dump_page", + description="Dump each dirty page and content - Default off", optional=True, ), ] @@ -60,22 +60,35 @@ class Malfind(interfaces.plugins.PluginInterface): return None proc_layer = self.context.layers[proc_layer_name] - dumpsize = self.config.get("dumpsize") if self.config.get("dumpsize") is not None else 64 - dumppage = self.config.get("dumppage") or False + + # Allowing a dump_size of 0 (no dump) + dump_size = self.config.get("dump_size") if self.config.get("dump_size") is not None else 64 + + # Dumping page defaults to off, as in case a whole r-xp region is dirty + # this would likely dump 1000's of pages which might not always be wise nor necessary + + dump_page = self.config.get("dump_page") or False for vma in task.mm.get_vma_iter(): vma_name = vma.get_name(self.context, task) vollog.debug( f"Injections : processing PID {task.pid} : VMA {vma_name} : {hex(vma.vm_start)}-{hex(vma.vm_end)}" ) + + # If is_suspicious returns true, this means at least one page + # in the region is dirty. If dump_page is true, then we dump + # all dirty pages + if vma.is_suspicious(proc_layer) and vma_name != "[vdso]": malicious_pages = vma.get_malicious_pages(proc_layer) - if dumppage: + if dump_page: + # Dumping each dirty page for page_addr in malicious_pages: - data = proc_layer.read(page_addr, dumpsize, pad=True) - yield vma, vma_name+f", page address: {page_addr:#x}, offset: {page_addr-vma.vm_start:#x}", data + data = proc_layer.read(page_addr, dump_size, pad=True) + yield vma, f"{vma_name}, page address: {page_addr:#x}, offset: {page_addr-vma.vm_start:#x}", data else: - data = proc_layer.read(vma.vm_start,dumpsize,pad=True) + # Original behaviour - Dump the start of the region (not necessarily matching the dirty page) + data = proc_layer.read(vma.vm_start,dump_size,pad=True) yield vma, vma_name, data def _generator(self, tasks): diff --git a/volatility3/framework/symbols/linux/extensions/__init__.py b/volatility3/framework/symbols/linux/extensions/__init__.py index 27cb44989..236d28bb1 100644 --- a/volatility3/framework/symbols/linux/extensions/__init__.py +++ b/volatility3/framework/symbols/linux/extensions/__init__.py @@ -1274,19 +1274,17 @@ class vm_area_struct(objects.StructType): return None def get_malicious_pages(self,proclayer=None): + """ + This function will return a list of all malicious pages inside a given dirty region + """ malicious_pages = [] - flags_str = self.get_protection() - if flags_str == "rwx": - ret = True - elif flags_str == "r-x" and self.vm_file.dereference().vol.offset == 0: - ret = True - elif proclayer and "x" in flags_str: + if proclayer and "r-x" in flags_str and self.vm_file.dereference().vol.offset !=0: for i in range(self.vm_start, self.vm_end, proclayer.page_size): try: if proclayer.is_dirty(i): - vollog.debug( + vollog.warning( f"Found malicious (dirty+exec) page at {hex(i)} !" ) malicious_pages.append(i) From ba9e13698d470e6c3502b12fec45f02dfc0e133f Mon Sep 17 00:00:00 2001 From: tvanegro Date: Tue, 17 Jun 2025 11:33:44 +0200 Subject: [PATCH 05/86] Minor changes 2 --- volatility3/framework/plugins/linux/malware/malfind.py | 8 ++++---- .../framework/symbols/linux/extensions/__init__.py | 3 +-- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/volatility3/framework/plugins/linux/malware/malfind.py b/volatility3/framework/plugins/linux/malware/malfind.py index 39d885f68..577c6c1e8 100644 --- a/volatility3/framework/plugins/linux/malware/malfind.py +++ b/volatility3/framework/plugins/linux/malware/malfind.py @@ -38,12 +38,12 @@ class Malfind(interfaces.plugins.PluginInterface): optional=True, ), requirements.IntRequirement( - name="dump_size", + name="dump-size", description="Amount of bytes to dump for each dirty region/page found - Default 64 bytes", optional=True, ), requirements.BooleanRequirement( - name="dump_page", + name="dump-page", description="Dump each dirty page and content - Default off", optional=True, ), @@ -62,12 +62,12 @@ class Malfind(interfaces.plugins.PluginInterface): proc_layer = self.context.layers[proc_layer_name] # Allowing a dump_size of 0 (no dump) - dump_size = self.config.get("dump_size") if self.config.get("dump_size") is not None else 64 + dump_size = self.config.get("dump-size") if self.config.get("dump-size") is not None else 64 # Dumping page defaults to off, as in case a whole r-xp region is dirty # this would likely dump 1000's of pages which might not always be wise nor necessary - dump_page = self.config.get("dump_page") or False + dump_page = self.config.get("dump-page") or False for vma in task.mm.get_vma_iter(): vma_name = vma.get_name(self.context, task) diff --git a/volatility3/framework/symbols/linux/extensions/__init__.py b/volatility3/framework/symbols/linux/extensions/__init__.py index 236d28bb1..94dfb0576 100644 --- a/volatility3/framework/symbols/linux/extensions/__init__.py +++ b/volatility3/framework/symbols/linux/extensions/__init__.py @@ -1284,7 +1284,7 @@ class vm_area_struct(objects.StructType): for i in range(self.vm_start, self.vm_end, proclayer.page_size): try: if proclayer.is_dirty(i): - vollog.warning( + vollog.debug( f"Found malicious (dirty+exec) page at {hex(i)} !" ) malicious_pages.append(i) @@ -2757,7 +2757,6 @@ class page(objects.StructType): for name, value in self.pageflags_enum.items(): if self.flags & (1 << value) != 0: flags.append(name) - print(name,value) return flags From 53b3bd7d47ca855a770be8be0119a6a86439a49b Mon Sep 17 00:00:00 2001 From: tvanegro Date: Tue, 17 Jun 2025 12:51:59 +0200 Subject: [PATCH 06/86] black --- .../framework/plugins/linux/malware/malfind.py | 8 ++++++-- .../framework/symbols/linux/extensions/__init__.py | 12 +++++++----- 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/volatility3/framework/plugins/linux/malware/malfind.py b/volatility3/framework/plugins/linux/malware/malfind.py index 577c6c1e8..4aaaf9bf8 100644 --- a/volatility3/framework/plugins/linux/malware/malfind.py +++ b/volatility3/framework/plugins/linux/malware/malfind.py @@ -62,7 +62,11 @@ class Malfind(interfaces.plugins.PluginInterface): proc_layer = self.context.layers[proc_layer_name] # Allowing a dump_size of 0 (no dump) - dump_size = self.config.get("dump-size") if self.config.get("dump-size") is not None else 64 + dump_size = ( + self.config.get("dump-size") + if self.config.get("dump-size") is not None + else 64 + ) # Dumping page defaults to off, as in case a whole r-xp region is dirty # this would likely dump 1000's of pages which might not always be wise nor necessary @@ -88,7 +92,7 @@ class Malfind(interfaces.plugins.PluginInterface): yield vma, f"{vma_name}, page address: {page_addr:#x}, offset: {page_addr-vma.vm_start:#x}", data else: # Original behaviour - Dump the start of the region (not necessarily matching the dirty page) - data = proc_layer.read(vma.vm_start,dump_size,pad=True) + data = proc_layer.read(vma.vm_start, dump_size, pad=True) yield vma, vma_name, data def _generator(self, tasks): diff --git a/volatility3/framework/symbols/linux/extensions/__init__.py b/volatility3/framework/symbols/linux/extensions/__init__.py index 94dfb0576..99b2c989c 100644 --- a/volatility3/framework/symbols/linux/extensions/__init__.py +++ b/volatility3/framework/symbols/linux/extensions/__init__.py @@ -1273,20 +1273,22 @@ class vm_area_struct(objects.StructType): except exceptions.InvalidAddressException: return None - def get_malicious_pages(self,proclayer=None): + def get_malicious_pages(self, proclayer=None): """ This function will return a list of all malicious pages inside a given dirty region """ malicious_pages = [] flags_str = self.get_protection() - if proclayer and "r-x" in flags_str and self.vm_file.dereference().vol.offset !=0: + if ( + proclayer + and "r-x" in flags_str + and self.vm_file.dereference().vol.offset != 0 + ): for i in range(self.vm_start, self.vm_end, proclayer.page_size): try: if proclayer.is_dirty(i): - vollog.debug( - f"Found malicious (dirty+exec) page at {hex(i)} !" - ) + vollog.debug(f"Found malicious (dirty+exec) page at {hex(i)} !") malicious_pages.append(i) except ( exceptions.PagedInvalidAddressException, From e13b8f9bd0633496dda13df1446fcafa6c9207e7 Mon Sep 17 00:00:00 2001 From: tvanegro Date: Tue, 17 Jun 2025 16:12:22 +0200 Subject: [PATCH 07/86] Fixing memory wrong memory offset in hex dump --- .../framework/plugins/linux/malware/malfind.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/volatility3/framework/plugins/linux/malware/malfind.py b/volatility3/framework/plugins/linux/malware/malfind.py index 4aaaf9bf8..98e9c4695 100644 --- a/volatility3/framework/plugins/linux/malware/malfind.py +++ b/volatility3/framework/plugins/linux/malware/malfind.py @@ -85,15 +85,17 @@ class Malfind(interfaces.plugins.PluginInterface): if vma.is_suspicious(proc_layer) and vma_name != "[vdso]": malicious_pages = vma.get_malicious_pages(proc_layer) + offset = 0 if dump_page: # Dumping each dirty page for page_addr in malicious_pages: + offset = page_addr - vma.vm_start data = proc_layer.read(page_addr, dump_size, pad=True) - yield vma, f"{vma_name}, page address: {page_addr:#x}, offset: {page_addr-vma.vm_start:#x}", data + yield vma, f"{vma_name}, page address: {page_addr:#x}, offset: {offset:#x}", data, offset else: # Original behaviour - Dump the start of the region (not necessarily matching the dirty page) data = proc_layer.read(vma.vm_start, dump_size, pad=True) - yield vma, vma_name, data + yield vma, vma_name, data, offset def _generator(self, tasks): # determine if we're on a 32 or 64 bit kernel @@ -105,13 +107,15 @@ class Malfind(interfaces.plugins.PluginInterface): for task in tasks: process_name = utility.array_to_string(task.comm) - for vma, vma_name, data in self._list_injections(task): + for vma, vma_name, data, offset in self._list_injections(task): if is_32bit_arch: architecture = "intel" else: architecture = "intel64" - disasm = renderers.Disassembly(data, vma.vm_start, architecture) + disasm = renderers.Disassembly( + data, vma.vm_start + offset, architecture + ) yield ( 0, From 50767ffb8d809b6b2c7fab1d599c95965422a5d7 Mon Sep 17 00:00:00 2001 From: kyrre Date: Fri, 27 Jun 2025 16:33:28 +0200 Subject: [PATCH 08/86] add arrow and parquet renderers --- pyproject.toml | 2 + volatility3/cli/text_renderer.py | 141 ++++++++++++++++++++++++++++++- 2 files changed, 142 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index b88ac7752..0acbfcbbe 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -42,6 +42,8 @@ dev = [ "types-jsonschema>=4.23.0,<5", ] +arrow = ["pyarrow>=17.0.0"] + test = [ "volatility3[dev]", "pytest>=8.3.3,<9", diff --git a/volatility3/cli/text_renderer.py b/volatility3/cli/text_renderer.py index 1e437a0be..69d2d6d31 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, Set, Tuple, TypeVar, Union +from typing import Any, Callable, Dict, List, Optional, Set, Tuple, TypeVar, Union, TextIO from volatility3.cli import text_filter from volatility3.framework import exceptions, interfaces, renderers @@ -25,6 +25,15 @@ except ImportError: vollog.debug("Disassembly library capstone not found") +try: + ARROW_PRESENT = True + import pyarrow as pa + import pyarrow.parquet as pq +except ImportError: + ARROW_PRESENT = False + vollog.debug("Arrow/Parquet libraries not found") + + def hex_bytes_as_text(value: bytes, width: int = 16) -> str: """Renders HexBytes as text. @@ -624,3 +633,133 @@ class JsonLinesRenderer(JsonRenderer): for line in result: outfd.write(json.dumps(line, sort_keys=True)) outfd.write("\n") + +class ArrowRenderer(CLIRenderer): + def __init__( + self, options: List[interfaces.renderers.RenderOption] | None = None + ) -> None: + super().__init__(options) + + if not ARROW_PRESENT: + raise RuntimeError("Arrow output format requires the pyarrow package") + + _to_arrow_type = { + renderers.Disassembly: pa.utf8, + bool: pa.bool_, + int: pa.int64, + float: pa.float64, + str: pa.utf8, + datetime.datetime: lambda: pa.timestamp("ms"), + format_hints.Bin: pa.int64, + format_hints.Hex: pa.int64, + format_hints.MultiTypeData: pa.utf8, + format_hints.HexBytes: pa.binary, + } + + name = "arrow" + structured_output = True + + def get_render_options(self) -> List[interfaces.renderers.RenderOption]: + return [] + + def to_arrow_schema(self, grid: interfaces.renderers.TreeGrid) -> pa.Schema: + fields = [] + for column in grid.columns: + arrow_type = self._to_arrow_type[column.type] + fields.append(pa.field(column.name, arrow_type())) + return pa.schema(fields) + + def output_result(self, schema, outfd: TextIO, result): + """Outputs the JSON data to a file in a particular format""" + + t = pa.Table.from_pylist(result, schema=schema) + self.write_data(t, outfd) + + def write_data(self, t: pa.Table, outfd: TextIO) -> None: + buf = pa.BufferOutputStream() + + writer = pa.ipc.new_stream(buf, t.schema) + writer.write_table(t) + writer.close() + + # Get the buffer bytes and write to output + buf_bytes = buf.getvalue().to_pybytes() + outfd.buffer.write(buf_bytes) + + def render(self, grid: interfaces.renderers.TreeGrid): + outfd = sys.stdout + final_output: Tuple[ + Dict[str, List[interfaces.renderers.TreeNode]], + List[interfaces.renderers.TreeNode], + ] = ({}, []) + + ignore_columns = self.ignored_columns(grid) + + def visitor( + node: interfaces.renderers.TreeNode, + accumulator: Tuple[Dict[str, Dict[str, Any]], List[Dict[str, Any]]], + ) -> Tuple[Dict[str, Dict[str, Any]], List[Dict[str, Any]]]: + # Nodes always have a path value, giving them a path_depth of at least 1, we use max just in case + acc_map, final_tree = accumulator + node_dict: Dict[str, Any] = {"__children": []} + line = [] + for column_index, column in enumerate(grid.columns): + if column in ignore_columns: + continue + + data = list(node.values)[column_index] + + if isinstance(data, interfaces.renderers.BaseAbsentValue): + data = None + + if isinstance(data, renderers.Disassembly): + data = display_disassembly(data) + + node_dict[column.name] = data + line.append(data) + + if self.filter and self.filter.filter(line): + return accumulator + + if node.parent: + acc_map[node.parent.path]["__children"].append(node_dict) + else: + final_tree.append(node_dict) + acc_map[node.path] = node_dict + + return (acc_map, final_tree) + + if not grid.populated: + grid.populate(visitor, final_output) + else: + grid.visit(node=None, function=visitor, initial_accumulator=final_output) + + schema = self.to_arrow_schema(grid) + self.output_result(schema, outfd, final_output[1]) + + +class ParquetRenderer(ArrowRenderer): + name = "parquet" + structured_output = True + + def get_render_options(self) -> List[interfaces.renderers.RenderOption]: + return [] + + def write_data(self, t: pa.Table, outfd: TextIO) -> None: + """ + Writes a table to stdout using the Parquet format. + + Args: + t: The Arrow table to write + outfd: The output file descriptor + + Returns: + Nothing + """ + # Write DataFrame to a temporary file-like object + buf = pa.BufferOutputStream() + pq.write_table(t, buf, compression="snappy") + + # Get the buffer as a bytes object + buf_bytes = buf.getvalue().to_pybytes() + outfd.buffer.write(buf_bytes) \ No newline at end of file From a0b00766f8df6cd4351e850ee9424a1c18786aa9 Mon Sep 17 00:00:00 2001 From: kyrre Date: Fri, 27 Jun 2025 16:58:35 +0200 Subject: [PATCH 09/86] formatting with ruff --- 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 69d2d6d31..13276520d 100644 --- a/volatility3/cli/text_renderer.py +++ b/volatility3/cli/text_renderer.py @@ -748,11 +748,11 @@ class ParquetRenderer(ArrowRenderer): def write_data(self, t: pa.Table, outfd: TextIO) -> None: """ Writes a table to stdout using the Parquet format. - + Args: t: The Arrow table to write outfd: The output file descriptor - + Returns: Nothing """ @@ -762,4 +762,4 @@ class ParquetRenderer(ArrowRenderer): # Get the buffer as a bytes object buf_bytes = buf.getvalue().to_pybytes() - outfd.buffer.write(buf_bytes) \ No newline at end of file + outfd.buffer.write(buf_bytes) From 1c7856212f6c05feec3aa4d5527b7b00f4bbc456 Mon Sep 17 00:00:00 2001 From: kyrre Date: Fri, 27 Jun 2025 17:01:08 +0200 Subject: [PATCH 10/86] linting --- volatility3/cli/text_renderer.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/volatility3/cli/text_renderer.py b/volatility3/cli/text_renderer.py index 13276520d..1bdca3803 100644 --- a/volatility3/cli/text_renderer.py +++ b/volatility3/cli/text_renderer.py @@ -9,7 +9,18 @@ import random import string import sys from functools import wraps -from typing import Any, Callable, Dict, List, Optional, Set, Tuple, TypeVar, Union, TextIO +from typing import ( + Any, + Callable, + Dict, + List, + Optional, + Set, + Tuple, + TypeVar, + Union, + TextIO, +) from volatility3.cli import text_filter from volatility3.framework import exceptions, interfaces, renderers @@ -634,6 +645,7 @@ class JsonLinesRenderer(JsonRenderer): outfd.write(json.dumps(line, sort_keys=True)) outfd.write("\n") + class ArrowRenderer(CLIRenderer): def __init__( self, options: List[interfaces.renderers.RenderOption] | None = None From 03895969875f682031466cb84793b6dc9d74f617 Mon Sep 17 00:00:00 2001 From: Kyrre-Wahl-Kongsgard Date: Sat, 28 Jun 2025 11:46:13 +0200 Subject: [PATCH 11/86] Fix: Avoid import-time errors if pyarrow is missing - Use string type annotations for pa.* types in Arrow/Parquet renderers. - Ensure pyarrow dependency is checked only in __init__. --- volatility3/cli/text_renderer.py | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/volatility3/cli/text_renderer.py b/volatility3/cli/text_renderer.py index 1bdca3803..d2334a9f9 100644 --- a/volatility3/cli/text_renderer.py +++ b/volatility3/cli/text_renderer.py @@ -655,18 +655,18 @@ class ArrowRenderer(CLIRenderer): if not ARROW_PRESENT: raise RuntimeError("Arrow output format requires the pyarrow package") - _to_arrow_type = { - renderers.Disassembly: pa.utf8, - bool: pa.bool_, - int: pa.int64, - float: pa.float64, - str: pa.utf8, - datetime.datetime: lambda: pa.timestamp("ms"), - format_hints.Bin: pa.int64, - format_hints.Hex: pa.int64, - format_hints.MultiTypeData: pa.utf8, - format_hints.HexBytes: pa.binary, - } + self._to_arrow_type = { + renderers.Disassembly: pa.utf8, + bool: pa.bool_, + int: pa.int64, + float: pa.float64, + str: pa.utf8, + datetime.datetime: lambda: pa.timestamp("ms"), + format_hints.Bin: pa.int64, + format_hints.Hex: pa.int64, + format_hints.MultiTypeData: pa.utf8, + format_hints.HexBytes: pa.binary, + } name = "arrow" structured_output = True @@ -674,20 +674,20 @@ class ArrowRenderer(CLIRenderer): def get_render_options(self) -> List[interfaces.renderers.RenderOption]: return [] - def to_arrow_schema(self, grid: interfaces.renderers.TreeGrid) -> pa.Schema: + def to_arrow_schema(self, grid: interfaces.renderers.TreeGrid) -> "pa.Schema": fields = [] for column in grid.columns: arrow_type = self._to_arrow_type[column.type] fields.append(pa.field(column.name, arrow_type())) return pa.schema(fields) - def output_result(self, schema, outfd: TextIO, result): + def output_result(self, schema: "pa.Schema", outfd: TextIO, result): """Outputs the JSON data to a file in a particular format""" t = pa.Table.from_pylist(result, schema=schema) self.write_data(t, outfd) - def write_data(self, t: pa.Table, outfd: TextIO) -> None: + def write_data(self, t: "pa.Table", outfd: TextIO) -> None: buf = pa.BufferOutputStream() writer = pa.ipc.new_stream(buf, t.schema) @@ -757,7 +757,7 @@ class ParquetRenderer(ArrowRenderer): def get_render_options(self) -> List[interfaces.renderers.RenderOption]: return [] - def write_data(self, t: pa.Table, outfd: TextIO) -> None: + def write_data(self, t: "pa.Table", outfd: TextIO) -> None: """ Writes a table to stdout using the Parquet format. From 78d56534c0b9b17fff2e9e62c06f53fb1edc7015 Mon Sep 17 00:00:00 2001 From: Kyrre-Wahl-Kongsgard Date: Sat, 28 Jun 2025 12:16:36 +0200 Subject: [PATCH 12/86] fix: handle layerdata to pyarrow conversion --- volatility3/cli/text_renderer.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/volatility3/cli/text_renderer.py b/volatility3/cli/text_renderer.py index d2334a9f9..52a499ffe 100644 --- a/volatility3/cli/text_renderer.py +++ b/volatility3/cli/text_renderer.py @@ -666,6 +666,8 @@ class ArrowRenderer(CLIRenderer): format_hints.Hex: pa.int64, format_hints.MultiTypeData: pa.utf8, format_hints.HexBytes: pa.binary, + renderers.LayerData: pa.binary, + bytes: pa.binary, } name = "arrow" @@ -727,6 +729,9 @@ class ArrowRenderer(CLIRenderer): if isinstance(data, renderers.Disassembly): data = display_disassembly(data) + if isinstance(data, renderers.LayerData): + data = LayerDataRenderer().render_bytes(data)[0] + node_dict[column.name] = data line.append(data) From e4aa9af834bedc70f7e27d2a8599444b2a7af4ab Mon Sep 17 00:00:00 2001 From: tvanegro Date: Thu, 3 Jul 2025 13:31:03 +0200 Subject: [PATCH 13/86] version bump --- volatility3/framework/plugins/linux/malware/malfind.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/volatility3/framework/plugins/linux/malware/malfind.py b/volatility3/framework/plugins/linux/malware/malfind.py index 98e9c4695..b9a03c616 100644 --- a/volatility3/framework/plugins/linux/malware/malfind.py +++ b/volatility3/framework/plugins/linux/malware/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, 3) + _version = (1, 0, 4) @classmethod def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]: From 2f8aecc025ec326aa9897c9762d099a51d171910 Mon Sep 17 00:00:00 2001 From: SolitudePy <47316655+SolitudePy@users.noreply.github.com> Date: Wed, 4 Jun 2025 22:13:28 +0300 Subject: [PATCH 14/86] linux_utilities show deleted fd + lsof files_only arg --- volatility3/framework/plugins/linux/lsof.py | 15 +++++++++++++-- volatility3/framework/symbols/linux/__init__.py | 17 +++++++++++++---- 2 files changed, 26 insertions(+), 6 deletions(-) diff --git a/volatility3/framework/plugins/linux/lsof.py b/volatility3/framework/plugins/linux/lsof.py index 283eabca0..1880bb09e 100644 --- a/volatility3/framework/plugins/linux/lsof.py +++ b/volatility3/framework/plugins/linux/lsof.py @@ -137,6 +137,12 @@ class Lsof(plugins.PluginInterface, timeliner.TimeLinerInterface): element_type=int, optional=True, ), + requirements.BooleanRequirement( + name="files_only", + description="Include only file descriptors of type file", + optional=True, + default=False, + ), ] @classmethod @@ -145,6 +151,7 @@ class Lsof(plugins.PluginInterface, timeliner.TimeLinerInterface): context: interfaces.context.ContextInterface, vmlinux_module_name: str, filter_func: Callable[[int], bool] = lambda _: False, + include_files_only: bool = False, ) -> Iterable[FDInternal]: """Enumerates open file descriptors in tasks @@ -167,7 +174,7 @@ class Lsof(plugins.PluginInterface, timeliner.TimeLinerInterface): linuxutils_symbol_table = task.vol.type_name.split(constants.BANG)[0] fd_generator = linux.LinuxUtilities.files_descriptors_for_process( - context, linuxutils_symbol_table, task + context, linuxutils_symbol_table, task, files_only=include_files_only ) for fd_fields in fd_generator: @@ -175,8 +182,12 @@ class Lsof(plugins.PluginInterface, timeliner.TimeLinerInterface): def _generator(self, pids, vmlinux_module_name): filter_func = pslist.PsList.create_pid_filter(pids) + include_files_only = self.config.get("files_only") for fd_internal in self.list_fds( - self.context, vmlinux_module_name, filter_func=filter_func + self.context, + vmlinux_module_name, + filter_func=filter_func, + include_files_only=include_files_only, ): fd_user = fd_internal.to_user() yield (0, dataclasses.astuple(fd_user)) diff --git a/volatility3/framework/symbols/linux/__init__.py b/volatility3/framework/symbols/linux/__init__.py index ac27b7e42..d66d350b1 100644 --- a/volatility3/framework/symbols/linux/__init__.py +++ b/volatility3/framework/symbols/linux/__init__.py @@ -101,6 +101,7 @@ class LinuxUtilities(interfaces.configuration.VersionableInterface): _version = (2, 3, 1) _required_framework_version = (2, 0, 0) + deleted = " (deleted)" framework.require_interface_version(*_required_framework_version) @@ -168,6 +169,7 @@ class LinuxUtilities(interfaces.configuration.VersionableInterface): # vfsmnt can be the vfsmount object itself (>=3.3) or a vfsmount * (<3.3) return "" + inode = dentry.d_inode path_reversed = [] smeared = False while ( @@ -191,6 +193,7 @@ class LinuxUtilities(interfaces.configuration.VersionableInterface): parent = dentry.d_parent dname = dentry.d_name.name_as_str() + # empty dentry names are most likely # the result of smearing if not dname: @@ -204,6 +207,9 @@ class LinuxUtilities(interfaces.configuration.VersionableInterface): # path would be /foo/bar/baz, but bar is missing due to smear the results # returned here will show /foo//baz. Note the // for the missing dname. return f" {path}" + print(path, inode.i_nlink) + if inode and inode.is_readable() and inode.is_valid() and inode.i_nlink == 0: + path += LinuxUtilities.deleted return path @classmethod @@ -260,7 +266,7 @@ class LinuxUtilities(interfaces.configuration.VersionableInterface): pre_name = name.dereference().cast( "string", max_length=255, errors="replace" ) - return "/" + pre_name + " (deleted)" + return "/" + pre_name + LinuxUtilities.deleted else: pre_name = "" @@ -301,7 +307,7 @@ class LinuxUtilities(interfaces.configuration.VersionableInterface): return f"{pre_name}:[{inode.i_ino:d}]" @classmethod - def path_for_file(cls, context, task, filp) -> str: + def path_for_file(cls, context, task, filp, files_only) -> str: """Returns a file (or sock pipe) pathname relative to the task's root directory. A 'file' structure doesn't have enough information to properly restore its @@ -340,7 +346,7 @@ class LinuxUtilities(interfaces.configuration.VersionableInterface): except exceptions.InvalidAddressException: dname_is_valid = False - if dname_is_valid: + if dname_is_valid and not files_only: ret = LinuxUtilities._get_new_sock_pipe_path(context, task, filp) else: ret = LinuxUtilities._get_path_file(task, filp) @@ -353,6 +359,7 @@ class LinuxUtilities(interfaces.configuration.VersionableInterface): context: interfaces.context.ContextInterface, symbol_table: str, task: interfaces.objects.ObjectInterface, + files_only: bool = False, ): try: files = task.files @@ -376,7 +383,9 @@ class LinuxUtilities(interfaces.configuration.VersionableInterface): for fd_num, filp in enumerate(fds): if filp and filp.is_readable(): - full_path = LinuxUtilities.path_for_file(context, task, filp) + full_path = LinuxUtilities.path_for_file( + context, task, filp, files_only + ) yield fd_num, filp, full_path From bf68bb6590e75569a75bedffc05189fa5d8dddda Mon Sep 17 00:00:00 2001 From: SolitudePy <47316655+SolitudePy@users.noreply.github.com> Date: Wed, 4 Jun 2025 22:18:29 +0300 Subject: [PATCH 15/86] debug print remove --- volatility3/framework/symbols/linux/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/volatility3/framework/symbols/linux/__init__.py b/volatility3/framework/symbols/linux/__init__.py index d66d350b1..dbea14d47 100644 --- a/volatility3/framework/symbols/linux/__init__.py +++ b/volatility3/framework/symbols/linux/__init__.py @@ -207,7 +207,7 @@ class LinuxUtilities(interfaces.configuration.VersionableInterface): # path would be /foo/bar/baz, but bar is missing due to smear the results # returned here will show /foo//baz. Note the // for the missing dname. return f" {path}" - print(path, inode.i_nlink) + if inode and inode.is_readable() and inode.is_valid() and inode.i_nlink == 0: path += LinuxUtilities.deleted return path From e29a01e6826a58b75c9f1f95e06a8e38b521cc21 Mon Sep 17 00:00:00 2001 From: SolitudePy <47316655+SolitudePy@users.noreply.github.com> Date: Wed, 4 Jun 2025 22:20:10 +0300 Subject: [PATCH 16/86] black --- volatility3/framework/symbols/linux/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/volatility3/framework/symbols/linux/__init__.py b/volatility3/framework/symbols/linux/__init__.py index dbea14d47..6de445c6b 100644 --- a/volatility3/framework/symbols/linux/__init__.py +++ b/volatility3/framework/symbols/linux/__init__.py @@ -207,7 +207,7 @@ class LinuxUtilities(interfaces.configuration.VersionableInterface): # path would be /foo/bar/baz, but bar is missing due to smear the results # returned here will show /foo//baz. Note the // for the missing dname. return f" {path}" - + if inode and inode.is_readable() and inode.is_valid() and inode.i_nlink == 0: path += LinuxUtilities.deleted return path From 2efb7f580a6a574fd5c942e3a78082ec94f80023 Mon Sep 17 00:00:00 2001 From: SolitudePy <47316655+SolitudePy@users.noreply.github.com> Date: Wed, 4 Jun 2025 22:27:18 +0300 Subject: [PATCH 17/86] declare default value :(( --- volatility3/framework/symbols/linux/extensions/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/volatility3/framework/symbols/linux/extensions/__init__.py b/volatility3/framework/symbols/linux/extensions/__init__.py index 3b9a73e7c..8330d70aa 100644 --- a/volatility3/framework/symbols/linux/extensions/__init__.py +++ b/volatility3/framework/symbols/linux/extensions/__init__.py @@ -1253,7 +1253,7 @@ class vm_area_struct(objects.StructType): def _do_get_name(self, context, task) -> str: if self.vm_file != 0: - fname = linux.LinuxUtilities.path_for_file(context, task, self.vm_file) + fname = linux.LinuxUtilities.path_for_file(context, task, self.vm_file, files_only=False) elif self.vm_start <= task.mm.start_brk and self.vm_end >= task.mm.brk: fname = "[heap]" elif self.vm_start <= task.mm.start_stack <= self.vm_end: From e0dc64938463722e3465a7dbfdd09bd053b66849 Mon Sep 17 00:00:00 2001 From: SolitudePy <47316655+SolitudePy@users.noreply.github.com> Date: Wed, 4 Jun 2025 22:28:27 +0300 Subject: [PATCH 18/86] black --- volatility3/framework/symbols/linux/extensions/__init__.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/volatility3/framework/symbols/linux/extensions/__init__.py b/volatility3/framework/symbols/linux/extensions/__init__.py index 8330d70aa..318424d5b 100644 --- a/volatility3/framework/symbols/linux/extensions/__init__.py +++ b/volatility3/framework/symbols/linux/extensions/__init__.py @@ -1253,7 +1253,9 @@ class vm_area_struct(objects.StructType): def _do_get_name(self, context, task) -> str: if self.vm_file != 0: - fname = linux.LinuxUtilities.path_for_file(context, task, self.vm_file, files_only=False) + fname = linux.LinuxUtilities.path_for_file( + context, task, self.vm_file, files_only=False + ) elif self.vm_start <= task.mm.start_brk and self.vm_end >= task.mm.brk: fname = "[heap]" elif self.vm_start <= task.mm.start_stack <= self.vm_end: From 23e4d35017b7ed3f73458d862c77cdc471e5a1bf Mon Sep 17 00:00:00 2001 From: SolitudePy <47316655+SolitudePy@users.noreply.github.com> Date: Wed, 11 Jun 2025 19:44:50 +0300 Subject: [PATCH 19/86] prepend for deleted sock & file instead of (deleted) --- volatility3/framework/symbols/linux/__init__.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/volatility3/framework/symbols/linux/__init__.py b/volatility3/framework/symbols/linux/__init__.py index 6de445c6b..b3a8935c6 100644 --- a/volatility3/framework/symbols/linux/__init__.py +++ b/volatility3/framework/symbols/linux/__init__.py @@ -101,7 +101,7 @@ class LinuxUtilities(interfaces.configuration.VersionableInterface): _version = (2, 3, 1) _required_framework_version = (2, 0, 0) - deleted = " (deleted)" + deleted = "" framework.require_interface_version(*_required_framework_version) @@ -209,7 +209,7 @@ class LinuxUtilities(interfaces.configuration.VersionableInterface): return f" {path}" if inode and inode.is_readable() and inode.is_valid() and inode.i_nlink == 0: - path += LinuxUtilities.deleted + path = f"{LinuxUtilities.deleted} {path}" return path @classmethod @@ -266,7 +266,7 @@ class LinuxUtilities(interfaces.configuration.VersionableInterface): pre_name = name.dereference().cast( "string", max_length=255, errors="replace" ) - return "/" + pre_name + LinuxUtilities.deleted + return f"{LinuxUtilities.deleted} /{pre_name}" else: pre_name = "" From 0a8f2cd23e15d046a37bb986c6394e7bb04d9ec2 Mon Sep 17 00:00:00 2001 From: SolitudePy <47316655+SolitudePy@users.noreply.github.com> Date: Wed, 11 Jun 2025 19:47:27 +0300 Subject: [PATCH 20/86] files_only default arg & minor version bump --- volatility3/framework/symbols/linux/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/volatility3/framework/symbols/linux/__init__.py b/volatility3/framework/symbols/linux/__init__.py index b3a8935c6..8e1b94a0a 100644 --- a/volatility3/framework/symbols/linux/__init__.py +++ b/volatility3/framework/symbols/linux/__init__.py @@ -99,7 +99,7 @@ class LinuxKernelIntermedSymbols(intermed.IntermediateSymbolTable): class LinuxUtilities(interfaces.configuration.VersionableInterface): """Class with multiple useful linux functions.""" - _version = (2, 3, 1) + _version = (2, 4, 0) _required_framework_version = (2, 0, 0) deleted = "" @@ -307,7 +307,7 @@ class LinuxUtilities(interfaces.configuration.VersionableInterface): return f"{pre_name}:[{inode.i_ino:d}]" @classmethod - def path_for_file(cls, context, task, filp, files_only) -> str: + def path_for_file(cls, context, task, filp, files_only=False) -> str: """Returns a file (or sock pipe) pathname relative to the task's root directory. A 'file' structure doesn't have enough information to properly restore its From c64c6229d1db02f880d35be479410b2c58b31847 Mon Sep 17 00:00:00 2001 From: SolitudePy <47316655+SolitudePy@users.noreply.github.com> Date: Wed, 11 Jun 2025 19:58:18 +0300 Subject: [PATCH 21/86] move config arg to run() --- volatility3/framework/plugins/linux/lsof.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/volatility3/framework/plugins/linux/lsof.py b/volatility3/framework/plugins/linux/lsof.py index 1880bb09e..d807d444e 100644 --- a/volatility3/framework/plugins/linux/lsof.py +++ b/volatility3/framework/plugins/linux/lsof.py @@ -180,9 +180,9 @@ class Lsof(plugins.PluginInterface, timeliner.TimeLinerInterface): for fd_fields in fd_generator: yield FDInternal(task=task, fd_fields=fd_fields) - def _generator(self, pids, vmlinux_module_name): + def _generator(self, pids, vmlinux_module_name, include_files_only): filter_func = pslist.PsList.create_pid_filter(pids) - include_files_only = self.config.get("files_only") + for fd_internal in self.list_fds( self.context, vmlinux_module_name, @@ -195,6 +195,7 @@ class Lsof(plugins.PluginInterface, timeliner.TimeLinerInterface): def run(self): pids = self.config.get("pid", None) vmlinux_module_name = self.config["kernel"] + include_files_only = self.config.get("files_only") tree_grid_args = [ ("PID", int), @@ -212,7 +213,10 @@ class Lsof(plugins.PluginInterface, timeliner.TimeLinerInterface): ("Size", int), ] return renderers.TreeGrid( - tree_grid_args, self._generator(pids, vmlinux_module_name) + tree_grid_args, + self._generator( + pids, vmlinux_module_name, include_files_only=include_files_only + ), ) def generate_timeline(self): From 606323f25a13ae5bd73cc07721ad2e59780e4f5d Mon Sep 17 00:00:00 2001 From: SolitudePy <47316655+SolitudePy@users.noreply.github.com> Date: Thu, 12 Jun 2025 21:09:25 +0300 Subject: [PATCH 22/86] remove code for next PR --- volatility3/framework/symbols/linux/__init__.py | 2 +- volatility3/framework/symbols/linux/extensions/__init__.py | 4 +--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/volatility3/framework/symbols/linux/__init__.py b/volatility3/framework/symbols/linux/__init__.py index 8e1b94a0a..19f8fc800 100644 --- a/volatility3/framework/symbols/linux/__init__.py +++ b/volatility3/framework/symbols/linux/__init__.py @@ -266,7 +266,7 @@ class LinuxUtilities(interfaces.configuration.VersionableInterface): pre_name = name.dereference().cast( "string", max_length=255, errors="replace" ) - return f"{LinuxUtilities.deleted} /{pre_name}" + return "/" + pre_name + " (deleted)" else: pre_name = "" diff --git a/volatility3/framework/symbols/linux/extensions/__init__.py b/volatility3/framework/symbols/linux/extensions/__init__.py index 318424d5b..3b9a73e7c 100644 --- a/volatility3/framework/symbols/linux/extensions/__init__.py +++ b/volatility3/framework/symbols/linux/extensions/__init__.py @@ -1253,9 +1253,7 @@ class vm_area_struct(objects.StructType): def _do_get_name(self, context, task) -> str: if self.vm_file != 0: - fname = linux.LinuxUtilities.path_for_file( - context, task, self.vm_file, files_only=False - ) + fname = linux.LinuxUtilities.path_for_file(context, task, self.vm_file) elif self.vm_start <= task.mm.start_brk and self.vm_end >= task.mm.brk: fname = "[heap]" elif self.vm_start <= task.mm.start_stack <= self.vm_end: From c4717075b68ce70ae9d01261165322a01a02928c Mon Sep 17 00:00:00 2001 From: SolitudePy <47316655+SolitudePy@users.noreply.github.com> Date: Thu, 12 Jun 2025 21:27:19 +0300 Subject: [PATCH 23/86] create potential smear tag --- volatility3/framework/symbols/linux/__init__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/volatility3/framework/symbols/linux/__init__.py b/volatility3/framework/symbols/linux/__init__.py index 19f8fc800..856be961e 100644 --- a/volatility3/framework/symbols/linux/__init__.py +++ b/volatility3/framework/symbols/linux/__init__.py @@ -102,6 +102,7 @@ class LinuxUtilities(interfaces.configuration.VersionableInterface): _version = (2, 4, 0) _required_framework_version = (2, 0, 0) deleted = "" + smear = "" framework.require_interface_version(*_required_framework_version) @@ -206,7 +207,7 @@ class LinuxUtilities(interfaces.configuration.VersionableInterface): # if there is smear the missing dname will be empty. e.g. if the normal # path would be /foo/bar/baz, but bar is missing due to smear the results # returned here will show /foo//baz. Note the // for the missing dname. - return f" {path}" + return f"{LinuxUtilities.smear} {path}" if inode and inode.is_readable() and inode.is_valid() and inode.i_nlink == 0: path = f"{LinuxUtilities.deleted} {path}" From 7ed7d43c424010a9e5e1875854189299a5c52cc8 Mon Sep 17 00:00:00 2001 From: SolitudePy <47316655+SolitudePy@users.noreply.github.com> Date: Mon, 23 Jun 2025 10:18:47 +0300 Subject: [PATCH 24/86] plugins: bump lsof minor version --- volatility3/framework/plugins/linux/lsof.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/volatility3/framework/plugins/linux/lsof.py b/volatility3/framework/plugins/linux/lsof.py index d807d444e..4b6f011ea 100644 --- a/volatility3/framework/plugins/linux/lsof.py +++ b/volatility3/framework/plugins/linux/lsof.py @@ -110,7 +110,7 @@ class Lsof(plugins.PluginInterface, timeliner.TimeLinerInterface): """Lists open files for each processes.""" _required_framework_version = (2, 0, 0) - _version = (2, 0, 2) + _version = (2, 1, 0) @classmethod def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]: From eb790083e2875f63dc14a9a9fb52f5088114f08e Mon Sep 17 00:00:00 2001 From: SolitudePy <47316655+SolitudePy@users.noreply.github.com> Date: Fri, 11 Jul 2025 15:26:03 +0300 Subject: [PATCH 25/86] plugins: lsof change back to (deleted) --- volatility3/framework/symbols/linux/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/volatility3/framework/symbols/linux/__init__.py b/volatility3/framework/symbols/linux/__init__.py index 856be961e..19fb8f1d4 100644 --- a/volatility3/framework/symbols/linux/__init__.py +++ b/volatility3/framework/symbols/linux/__init__.py @@ -101,7 +101,7 @@ class LinuxUtilities(interfaces.configuration.VersionableInterface): _version = (2, 4, 0) _required_framework_version = (2, 0, 0) - deleted = "" + deleted = "(deleted)" smear = "" framework.require_interface_version(*_required_framework_version) @@ -210,7 +210,7 @@ class LinuxUtilities(interfaces.configuration.VersionableInterface): return f"{LinuxUtilities.smear} {path}" if inode and inode.is_readable() and inode.is_valid() and inode.i_nlink == 0: - path = f"{LinuxUtilities.deleted} {path}" + path = f" {path} {LinuxUtilities.deleted}" return path @classmethod From d3a6b030b7604f6289f59dc7fa3e2882eb69225a Mon Sep 17 00:00:00 2001 From: Kyrre-Wahl-Kongsgard Date: Mon, 14 Jul 2025 13:38:35 +0200 Subject: [PATCH 26/86] moved arrow/parquet parsers to it's own file and reverted changes --- volatility3/cli/text_renderer.py | 166 +----------------- .../framework/plugins/parquet_renderer.py | 161 +++++++++++++++++ 2 files changed, 165 insertions(+), 162 deletions(-) create mode 100644 volatility3/framework/plugins/parquet_renderer.py diff --git a/volatility3/cli/text_renderer.py b/volatility3/cli/text_renderer.py index 52a499ffe..89526c90c 100644 --- a/volatility3/cli/text_renderer.py +++ b/volatility3/cli/text_renderer.py @@ -9,18 +9,7 @@ import random import string import sys from functools import wraps -from typing import ( - Any, - Callable, - Dict, - List, - Optional, - Set, - Tuple, - TypeVar, - Union, - TextIO, -) +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 @@ -36,15 +25,6 @@ except ImportError: vollog.debug("Disassembly library capstone not found") -try: - ARROW_PRESENT = True - import pyarrow as pa - import pyarrow.parquet as pq -except ImportError: - ARROW_PRESENT = False - vollog.debug("Arrow/Parquet libraries not found") - - def hex_bytes_as_text(value: bytes, width: int = 16) -> str: """Renders HexBytes as text. @@ -298,7 +278,6 @@ class CLIRenderer(interfaces.renderers.Renderer): class QuickTextRenderer(CLIRenderer): - name = "quick" def get_render_options(self): @@ -368,7 +347,6 @@ class NoneRenderer(CLIRenderer): class CSVRenderer(CLIRenderer): - name = "csv" structured_output = True @@ -486,9 +464,9 @@ class PrettyTextRenderer(CLIRenderer): accumulator.append((node.path_depth, line)) return accumulator - final_output: List[Tuple[int, Dict[interfaces.renderers.Column, list[str]]]] = ( - [] - ) + final_output: List[ + Tuple[int, Dict[interfaces.renderers.Column, list[str]]] + ] = [] if not grid.populated: grid.populate(visitor, final_output) else: @@ -644,139 +622,3 @@ class JsonLinesRenderer(JsonRenderer): for line in result: outfd.write(json.dumps(line, sort_keys=True)) outfd.write("\n") - - -class ArrowRenderer(CLIRenderer): - def __init__( - self, options: List[interfaces.renderers.RenderOption] | None = None - ) -> None: - super().__init__(options) - - if not ARROW_PRESENT: - raise RuntimeError("Arrow output format requires the pyarrow package") - - self._to_arrow_type = { - renderers.Disassembly: pa.utf8, - bool: pa.bool_, - int: pa.int64, - float: pa.float64, - str: pa.utf8, - datetime.datetime: lambda: pa.timestamp("ms"), - format_hints.Bin: pa.int64, - format_hints.Hex: pa.int64, - format_hints.MultiTypeData: pa.utf8, - format_hints.HexBytes: pa.binary, - renderers.LayerData: pa.binary, - bytes: pa.binary, - } - - name = "arrow" - structured_output = True - - def get_render_options(self) -> List[interfaces.renderers.RenderOption]: - return [] - - def to_arrow_schema(self, grid: interfaces.renderers.TreeGrid) -> "pa.Schema": - fields = [] - for column in grid.columns: - arrow_type = self._to_arrow_type[column.type] - fields.append(pa.field(column.name, arrow_type())) - return pa.schema(fields) - - def output_result(self, schema: "pa.Schema", outfd: TextIO, result): - """Outputs the JSON data to a file in a particular format""" - - t = pa.Table.from_pylist(result, schema=schema) - self.write_data(t, outfd) - - def write_data(self, t: "pa.Table", outfd: TextIO) -> None: - buf = pa.BufferOutputStream() - - writer = pa.ipc.new_stream(buf, t.schema) - writer.write_table(t) - writer.close() - - # Get the buffer bytes and write to output - buf_bytes = buf.getvalue().to_pybytes() - outfd.buffer.write(buf_bytes) - - def render(self, grid: interfaces.renderers.TreeGrid): - outfd = sys.stdout - final_output: Tuple[ - Dict[str, List[interfaces.renderers.TreeNode]], - List[interfaces.renderers.TreeNode], - ] = ({}, []) - - ignore_columns = self.ignored_columns(grid) - - def visitor( - node: interfaces.renderers.TreeNode, - accumulator: Tuple[Dict[str, Dict[str, Any]], List[Dict[str, Any]]], - ) -> Tuple[Dict[str, Dict[str, Any]], List[Dict[str, Any]]]: - # Nodes always have a path value, giving them a path_depth of at least 1, we use max just in case - acc_map, final_tree = accumulator - node_dict: Dict[str, Any] = {"__children": []} - line = [] - for column_index, column in enumerate(grid.columns): - if column in ignore_columns: - continue - - data = list(node.values)[column_index] - - if isinstance(data, interfaces.renderers.BaseAbsentValue): - data = None - - if isinstance(data, renderers.Disassembly): - data = display_disassembly(data) - - if isinstance(data, renderers.LayerData): - data = LayerDataRenderer().render_bytes(data)[0] - - node_dict[column.name] = data - line.append(data) - - if self.filter and self.filter.filter(line): - return accumulator - - if node.parent: - acc_map[node.parent.path]["__children"].append(node_dict) - else: - final_tree.append(node_dict) - acc_map[node.path] = node_dict - - return (acc_map, final_tree) - - if not grid.populated: - grid.populate(visitor, final_output) - else: - grid.visit(node=None, function=visitor, initial_accumulator=final_output) - - schema = self.to_arrow_schema(grid) - self.output_result(schema, outfd, final_output[1]) - - -class ParquetRenderer(ArrowRenderer): - name = "parquet" - structured_output = True - - def get_render_options(self) -> List[interfaces.renderers.RenderOption]: - return [] - - def write_data(self, t: "pa.Table", outfd: TextIO) -> None: - """ - Writes a table to stdout using the Parquet format. - - Args: - t: The Arrow table to write - outfd: The output file descriptor - - Returns: - Nothing - """ - # Write DataFrame to a temporary file-like object - buf = pa.BufferOutputStream() - pq.write_table(t, buf, compression="snappy") - - # Get the buffer as a bytes object - buf_bytes = buf.getvalue().to_pybytes() - outfd.buffer.write(buf_bytes) diff --git a/volatility3/framework/plugins/parquet_renderer.py b/volatility3/framework/plugins/parquet_renderer.py new file mode 100644 index 000000000..806dcd801 --- /dev/null +++ b/volatility3/framework/plugins/parquet_renderer.py @@ -0,0 +1,161 @@ +# This file is Copyright 2019 Volatility Foundation and licensed under the Volatility Software License 1.0 +# which is available at https://www.volatilityfoundation.org/license/vsl-v1.0 +# +import datetime +import logging +import sys +from typing import ( + Any, + Dict, + List, + Tuple, + TextIO, +) +from volatility3.framework import interfaces, renderers +from volatility3.framework.renderers import format_hints +from volatility3.cli.text_renderer import CLIRenderer, display_disassembly, LayerDataRenderer + +vollog = logging.getLogger(__name__) + +try: + ARROW_PRESENT = True + import pyarrow as pa + import pyarrow.parquet as pq +except ImportError: + ARROW_PRESENT = False + vollog.debug("Arrow/Parquet libraries not found") + +class ArrowRenderer(CLIRenderer): + def __init__( + self, options: List[interfaces.renderers.RenderOption] | None = None + ) -> None: + super().__init__(options) + + if not ARROW_PRESENT: + raise RuntimeError("Arrow output format requires the pyarrow package") + + self._to_arrow_type = { + renderers.Disassembly: pa.utf8, + bool: pa.bool_, + int: pa.int64, + float: pa.float64, + str: pa.utf8, + datetime.datetime: lambda: pa.timestamp("ms"), + format_hints.Bin: pa.int64, + format_hints.Hex: pa.int64, + format_hints.MultiTypeData: pa.utf8, + format_hints.HexBytes: pa.binary, + renderers.LayerData: pa.binary, + bytes: pa.binary, + } + + name = "arrow" + structured_output = True + + def get_render_options(self) -> List[interfaces.renderers.RenderOption]: + return [] + + def to_arrow_schema(self, grid: interfaces.renderers.TreeGrid) -> "pa.Schema": + fields = [] + for column in grid.columns: + arrow_type = self._to_arrow_type[column.type] + fields.append(pa.field(column.name, arrow_type())) + return pa.schema(fields) + + def output_result(self, schema: "pa.Schema", outfd: TextIO, result): + """Outputs the JSON data to a file in a particular format""" + + t = pa.Table.from_pylist(result, schema=schema) + self.write_data(t, outfd) + + def write_data(self, t: "pa.Table", outfd: TextIO) -> None: + buf = pa.BufferOutputStream() + + writer = pa.ipc.new_stream(buf, t.schema) + writer.write_table(t) + writer.close() + + # Get the buffer bytes and write to output + buf_bytes = buf.getvalue().to_pybytes() + outfd.buffer.write(buf_bytes) + + def render(self, grid: interfaces.renderers.TreeGrid): + outfd = sys.stdout + final_output: Tuple[ + Dict[str, List[interfaces.renderers.TreeNode]], + List[interfaces.renderers.TreeNode], + ] = ({}, []) + + ignore_columns = self.ignored_columns(grid) + + def visitor( + node: interfaces.renderers.TreeNode, + accumulator: Tuple[Dict[str, Dict[str, Any]], List[Dict[str, Any]]], + ) -> Tuple[Dict[str, Dict[str, Any]], List[Dict[str, Any]]]: + # Nodes always have a path value, giving them a path_depth of at least 1, we use max just in case + acc_map, final_tree = accumulator + node_dict: Dict[str, Any] = {"__children": []} + line = [] + for column_index, column in enumerate(grid.columns): + if column in ignore_columns: + continue + + data = list(node.values)[column_index] + + if isinstance(data, interfaces.renderers.BaseAbsentValue): + data = None + + if isinstance(data, renderers.Disassembly): + data = display_disassembly(data) + + if isinstance(data, renderers.LayerData): + data = LayerDataRenderer().render_bytes(data)[0] + + node_dict[column.name] = data + line.append(data) + + if self.filter and self.filter.filter(line): + return accumulator + + if node.parent: + acc_map[node.parent.path]["__children"].append(node_dict) + else: + final_tree.append(node_dict) + acc_map[node.path] = node_dict + + return (acc_map, final_tree) + + if not grid.populated: + grid.populate(visitor, final_output) + else: + grid.visit(node=None, function=visitor, initial_accumulator=final_output) + + schema = self.to_arrow_schema(grid) + self.output_result(schema, outfd, final_output[1]) + + +class ParquetRenderer(ArrowRenderer): + name = "parquet" + structured_output = True + + def get_render_options(self) -> List[interfaces.renderers.RenderOption]: + return [] + + def write_table(self, table: "pa.Table", outfd: TextIO) -> None: + """ + Writes a table to stdout using the Parquet format. + + Args: + t: The Arrow table to write + outfd: The output file descriptor + + Returns: + Nothing + """ + # Write DataFrame to a temporary file-like object + buf = pa.BufferOutputStream() + pq.write_table(table, buf, compression="snappy") + + # Get the buffer as a bytes object + buf_bytes = buf.getvalue().to_pybytes() + outfd.buffer.write(buf_bytes) From 8479fa1c136c3f71c7fb8602184219326fca1fae Mon Sep 17 00:00:00 2001 From: Kyrre-Wahl-Kongsgard Date: Mon, 14 Jul 2025 14:07:44 +0200 Subject: [PATCH 27/86] moved renderer parsing to after the plugins are imported --- volatility3/cli/__init__.py | 45 ++++++++++++++++++++----------------- 1 file changed, 25 insertions(+), 20 deletions(-) diff --git a/volatility3/cli/__init__.py b/volatility3/cli/__init__.py index cc97c4fcb..4c379a15e 100644 --- a/volatility3/cli/__init__.py +++ b/volatility3/cli/__init__.py @@ -106,13 +106,6 @@ class CommandLine: volatility3.framework.require_interface_version(2, 0, 0) - renderers = dict( - [ - (x.name.lower(), x) - for x in framework.class_subclasses(text_renderer.CLIRenderer) - ] - ) - # Load up system defaults delayed_logs, default_config = self.load_system_defaults("vol.json") @@ -193,14 +186,6 @@ class CommandLine: default=False, action="store_true", ) - parser.add_argument( - "-r", - "--renderer", - metavar="RENDERER", - help=f"Determines how to render the output ({', '.join(list(renderers))})", - default="quick", - choices=list(renderers), - ) parser.add_argument( "-f", "--file", @@ -270,11 +255,6 @@ class CommandLine: known_args = [arg for arg in sys.argv if arg != "--help" and arg != "-h"] partial_args, _ = parser.parse_known_args(known_args) - banner_output = sys.stdout - if renderers[partial_args.renderer].structured_output: - banner_output = sys.stderr - banner_output.write(f"Volatility 3 Framework {constants.PACKAGE_VERSION}\n") - ### Start up logging if partial_args.log: file_logger = logging.FileHandler(partial_args.log) @@ -346,6 +326,24 @@ class CommandLine: plugin_list = framework.list_plugins() + # Discover renderers after plugin directories are loaded + # This allows custom renderers to be found in plugin directories + renderers = dict( + [ + (x.name.lower(), x) + for x in framework.class_subclasses(text_renderer.CLIRenderer) + ] + ) + + parser.add_argument( + "-r", + "--renderer", + metavar="RENDERER", + help=f"Determines how to render the output ({', '.join(list(renderers))})", + default="quick", + choices=list(renderers), + ) + seen_automagics = set() chosen_configurables_list = {} for amagic in automagics: @@ -392,6 +390,13 @@ class CommandLine: # before all the plugins have been added argcomplete.autocomplete(parser) args = parser.parse_args() + + # Display banner - redirect to stderr if using structured output + banner_output = sys.stdout + if renderers[args.renderer].structured_output: + banner_output = sys.stderr + banner_output.write(f"Volatility 3 Framework {constants.PACKAGE_VERSION}\n") + if args.plugin is None: parser.error( f"Please select a plugin to run (see '{self.CLI_NAME} --help' for options" From 066076b4d83ff47b766f250bef379076ab8b8369 Mon Sep 17 00:00:00 2001 From: Kyrre-Wahl-Kongsgard Date: Mon, 14 Jul 2025 14:10:31 +0200 Subject: [PATCH 28/86] black formatting --- volatility3/cli/text_renderer.py | 6 +++--- volatility3/framework/plugins/parquet_renderer.py | 7 ++++++- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/volatility3/cli/text_renderer.py b/volatility3/cli/text_renderer.py index 89526c90c..d55201371 100644 --- a/volatility3/cli/text_renderer.py +++ b/volatility3/cli/text_renderer.py @@ -464,9 +464,9 @@ class PrettyTextRenderer(CLIRenderer): accumulator.append((node.path_depth, line)) return accumulator - final_output: List[ - Tuple[int, Dict[interfaces.renderers.Column, list[str]]] - ] = [] + final_output: List[Tuple[int, Dict[interfaces.renderers.Column, list[str]]]] = ( + [] + ) if not grid.populated: grid.populate(visitor, final_output) else: diff --git a/volatility3/framework/plugins/parquet_renderer.py b/volatility3/framework/plugins/parquet_renderer.py index 806dcd801..adc3a97e1 100644 --- a/volatility3/framework/plugins/parquet_renderer.py +++ b/volatility3/framework/plugins/parquet_renderer.py @@ -13,7 +13,11 @@ from typing import ( ) from volatility3.framework import interfaces, renderers from volatility3.framework.renderers import format_hints -from volatility3.cli.text_renderer import CLIRenderer, display_disassembly, LayerDataRenderer +from volatility3.cli.text_renderer import ( + CLIRenderer, + display_disassembly, + LayerDataRenderer, +) vollog = logging.getLogger(__name__) @@ -25,6 +29,7 @@ except ImportError: ARROW_PRESENT = False vollog.debug("Arrow/Parquet libraries not found") + class ArrowRenderer(CLIRenderer): def __init__( self, options: List[interfaces.renderers.RenderOption] | None = None From 0f32bec2638e69ac543f27f0ce2b0993cc9d75c6 Mon Sep 17 00:00:00 2001 From: Kyrre-Wahl-Kongsgard Date: Mon, 14 Jul 2025 14:20:37 +0200 Subject: [PATCH 29/86] restore orginal text_renderer --- volatility3/cli/text_renderer.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/volatility3/cli/text_renderer.py b/volatility3/cli/text_renderer.py index d55201371..1e437a0be 100644 --- a/volatility3/cli/text_renderer.py +++ b/volatility3/cli/text_renderer.py @@ -278,6 +278,7 @@ class CLIRenderer(interfaces.renderers.Renderer): class QuickTextRenderer(CLIRenderer): + name = "quick" def get_render_options(self): @@ -347,6 +348,7 @@ class NoneRenderer(CLIRenderer): class CSVRenderer(CLIRenderer): + name = "csv" structured_output = True From 72422d0db9ab255a03d50f3830681011c8b3191c Mon Sep 17 00:00:00 2001 From: Kyrre-Wahl-Kongsgard Date: Mon, 21 Jul 2025 13:38:54 +0200 Subject: [PATCH 30/86] fix bug: hadn't properly renamed the write_data method name --- volatility3/framework/plugins/parquet_renderer.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/volatility3/framework/plugins/parquet_renderer.py b/volatility3/framework/plugins/parquet_renderer.py index adc3a97e1..6564dbaeb 100644 --- a/volatility3/framework/plugins/parquet_renderer.py +++ b/volatility3/framework/plugins/parquet_renderer.py @@ -71,9 +71,9 @@ class ArrowRenderer(CLIRenderer): """Outputs the JSON data to a file in a particular format""" t = pa.Table.from_pylist(result, schema=schema) - self.write_data(t, outfd) + self.write_table(t, outfd) - def write_data(self, t: "pa.Table", outfd: TextIO) -> None: + def write_table(self, t: "pa.Table", outfd: TextIO) -> None: buf = pa.BufferOutputStream() writer = pa.ipc.new_stream(buf, t.schema) From 60c058548e9cbc718d73032cd7bc8aaa224112be Mon Sep 17 00:00:00 2001 From: Abyss Watcher Date: Mon, 21 Jul 2025 20:43:00 +0200 Subject: [PATCH 31/86] use mnt_node instead of mnt_list --- volatility3/framework/symbols/linux/extensions/__init__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/volatility3/framework/symbols/linux/extensions/__init__.py b/volatility3/framework/symbols/linux/extensions/__init__.py index 3b9a73e7c..e642c20ec 100644 --- a/volatility3/framework/symbols/linux/extensions/__init__.py +++ b/volatility3/framework/symbols/linux/extensions/__init__.py @@ -1971,8 +1971,9 @@ class mnt_namespace(objects.StructType): self._context, self ) for node in self.mounts.get_nodes(): + # See kernel's node_to_mount() mnt = linux.LinuxUtilities.container_of( - node, "mount", "mnt_list", vmlinux + node, "mount", "mnt_node", vmlinux ) yield mnt else: From e7c1126b05996ecb3fd718ba559422d68c852023 Mon Sep 17 00:00:00 2001 From: Kyrre-Wahl-Kongsgard Date: Mon, 21 Jul 2025 23:46:14 +0200 Subject: [PATCH 32/86] moved the arrow/parquet renderers to a subdir for renderers --- .../plugins/renderers/parquet_renderer.py | 173 ++++++++++++++++++ 1 file changed, 173 insertions(+) create mode 100644 volatility3/framework/plugins/renderers/parquet_renderer.py diff --git a/volatility3/framework/plugins/renderers/parquet_renderer.py b/volatility3/framework/plugins/renderers/parquet_renderer.py new file mode 100644 index 000000000..c9ef95ee7 --- /dev/null +++ b/volatility3/framework/plugins/renderers/parquet_renderer.py @@ -0,0 +1,173 @@ +# This file is Copyright 2019 Volatility Foundation and licensed under the Volatility Software License 1.0 +# which is available at https://www.volatilityfoundation.org/license/vsl-v1.0 +# +import datetime +import logging +import sys +from typing import ( + Any, + Dict, + List, + Tuple, + TextIO, +) +from volatility3.framework import interfaces, renderers +from volatility3.framework.renderers import format_hints +from volatility3.cli.text_renderer import ( + CLIRenderer, + display_disassembly, + LayerDataRenderer, +) + +vollog = logging.getLogger(__name__) + +ARROW_PRESENT = False +try: + import pyarrow as pa + import pyarrow.parquet as pq + + ARROW_PRESENT = True +except ImportError: + vollog.debug("Arrow/Parquet libraries not found") + + +class ArrowRenderer(CLIRenderer): + """Renderer that outputs Arrow IPC format data.""" + + name = "arrow" + structured_output = True + _version = (1, 0, 0) + + def __init__( + self, options: List[interfaces.renderers.RenderOption] | None = None + ) -> None: + super().__init__(options) + + if not ARROW_PRESENT: + raise RuntimeError("Arrow output format requires the pyarrow package") + + self._to_arrow_type = { + renderers.Disassembly: pa.utf8, + bool: pa.bool_, + int: pa.int64, + float: pa.float64, + str: pa.utf8, + datetime.datetime: lambda: pa.timestamp("ms"), + format_hints.Bin: pa.int64, + format_hints.Hex: pa.int64, + format_hints.MultiTypeData: pa.utf8, + format_hints.HexBytes: pa.binary, + renderers.LayerData: pa.binary, + bytes: pa.binary, + } + + def get_render_options(self) -> List[interfaces.renderers.RenderOption]: + return [] + + def to_arrow_schema(self, grid: interfaces.renderers.TreeGrid) -> "pa.Schema": + fields = [] + for column in grid.columns: + arrow_type = self._to_arrow_type[column.type] + fields.append(pa.field(column.name, arrow_type())) + return pa.schema(fields) + + def output_result(self, schema: "pa.Schema", outfd: TextIO, result): + """Outputs the JSON data to a file in a particular format""" + + t = pa.Table.from_pylist(result, schema=schema) + self.write_table(t, outfd) + + def write_table(self, t: "pa.Table", outfd: TextIO) -> None: + buf = pa.BufferOutputStream() + + writer = pa.ipc.new_stream(buf, t.schema) + writer.write_table(t) + writer.close() + + # Get the buffer bytes and write to output + buf_bytes = buf.getvalue().to_pybytes() + outfd.buffer.write(buf_bytes) + + def render(self, grid: interfaces.renderers.TreeGrid): + outfd = sys.stdout + final_output: Tuple[ + Dict[str, List[interfaces.renderers.TreeNode]], + List[interfaces.renderers.TreeNode], + ] = ({}, []) + + ignore_columns = self.ignored_columns(grid) + + def visitor( + node: interfaces.renderers.TreeNode, + accumulator: Tuple[Dict[str, Dict[str, Any]], List[Dict[str, Any]]], + ) -> Tuple[Dict[str, Dict[str, Any]], List[Dict[str, Any]]]: + # Nodes always have a path value, giving them a path_depth of at least 1, we use max just in case + acc_map, final_tree = accumulator + node_dict: Dict[str, Any] = {"__children": []} + line = [] + for column_index, column in enumerate(grid.columns): + if column in ignore_columns: + continue + + data = list(node.values)[column_index] + + if isinstance(data, interfaces.renderers.BaseAbsentValue): + data = None + + if isinstance(data, renderers.Disassembly): + data = display_disassembly(data) + + if isinstance(data, renderers.LayerData): + data = LayerDataRenderer().render_bytes(data)[0] + + node_dict[column.name] = data + line.append(data) + + if self.filter and self.filter.filter(line): + return accumulator + + if node.parent: + acc_map[node.parent.path]["__children"].append(node_dict) + else: + final_tree.append(node_dict) + acc_map[node.path] = node_dict + + return (acc_map, final_tree) + + if not grid.populated: + grid.populate(visitor, final_output) + else: + grid.visit(node=None, function=visitor, initial_accumulator=final_output) + + schema = self.to_arrow_schema(grid) + self.output_result(schema, outfd, final_output[1]) + + +class ParquetRenderer(ArrowRenderer): + """Renderer that outputs Parquet format data.""" + + name = "parquet" + structured_output = True + _version = (1, 0, 0) + + def get_render_options(self) -> List[interfaces.renderers.RenderOption]: + return [] + + def write_table(self, table: "pa.Table", outfd: TextIO) -> None: + """ + Writes a table to stdout using the Parquet format. + + Args: + t: The Arrow table to write + outfd: The output file descriptor + + Returns: + Nothing + """ + # Write DataFrame to a temporary file-like object + buf = pa.BufferOutputStream() + pq.write_table(table, buf, compression="snappy") + + # Get the buffer as a bytes object + buf_bytes = buf.getvalue().to_pybytes() + outfd.buffer.write(buf_bytes) From 6437148dc8d5d976e8f36a7b3e18780833de0716 Mon Sep 17 00:00:00 2001 From: Kyrre-Wahl-Kongsgard Date: Mon, 21 Jul 2025 23:47:24 +0200 Subject: [PATCH 33/86] remove old parquet renderer --- .../framework/plugins/parquet_renderer.py | 166 ------------------ 1 file changed, 166 deletions(-) delete mode 100644 volatility3/framework/plugins/parquet_renderer.py diff --git a/volatility3/framework/plugins/parquet_renderer.py b/volatility3/framework/plugins/parquet_renderer.py deleted file mode 100644 index 6564dbaeb..000000000 --- a/volatility3/framework/plugins/parquet_renderer.py +++ /dev/null @@ -1,166 +0,0 @@ -# This file is Copyright 2019 Volatility Foundation and licensed under the Volatility Software License 1.0 -# which is available at https://www.volatilityfoundation.org/license/vsl-v1.0 -# -import datetime -import logging -import sys -from typing import ( - Any, - Dict, - List, - Tuple, - TextIO, -) -from volatility3.framework import interfaces, renderers -from volatility3.framework.renderers import format_hints -from volatility3.cli.text_renderer import ( - CLIRenderer, - display_disassembly, - LayerDataRenderer, -) - -vollog = logging.getLogger(__name__) - -try: - ARROW_PRESENT = True - import pyarrow as pa - import pyarrow.parquet as pq -except ImportError: - ARROW_PRESENT = False - vollog.debug("Arrow/Parquet libraries not found") - - -class ArrowRenderer(CLIRenderer): - def __init__( - self, options: List[interfaces.renderers.RenderOption] | None = None - ) -> None: - super().__init__(options) - - if not ARROW_PRESENT: - raise RuntimeError("Arrow output format requires the pyarrow package") - - self._to_arrow_type = { - renderers.Disassembly: pa.utf8, - bool: pa.bool_, - int: pa.int64, - float: pa.float64, - str: pa.utf8, - datetime.datetime: lambda: pa.timestamp("ms"), - format_hints.Bin: pa.int64, - format_hints.Hex: pa.int64, - format_hints.MultiTypeData: pa.utf8, - format_hints.HexBytes: pa.binary, - renderers.LayerData: pa.binary, - bytes: pa.binary, - } - - name = "arrow" - structured_output = True - - def get_render_options(self) -> List[interfaces.renderers.RenderOption]: - return [] - - def to_arrow_schema(self, grid: interfaces.renderers.TreeGrid) -> "pa.Schema": - fields = [] - for column in grid.columns: - arrow_type = self._to_arrow_type[column.type] - fields.append(pa.field(column.name, arrow_type())) - return pa.schema(fields) - - def output_result(self, schema: "pa.Schema", outfd: TextIO, result): - """Outputs the JSON data to a file in a particular format""" - - t = pa.Table.from_pylist(result, schema=schema) - self.write_table(t, outfd) - - def write_table(self, t: "pa.Table", outfd: TextIO) -> None: - buf = pa.BufferOutputStream() - - writer = pa.ipc.new_stream(buf, t.schema) - writer.write_table(t) - writer.close() - - # Get the buffer bytes and write to output - buf_bytes = buf.getvalue().to_pybytes() - outfd.buffer.write(buf_bytes) - - def render(self, grid: interfaces.renderers.TreeGrid): - outfd = sys.stdout - final_output: Tuple[ - Dict[str, List[interfaces.renderers.TreeNode]], - List[interfaces.renderers.TreeNode], - ] = ({}, []) - - ignore_columns = self.ignored_columns(grid) - - def visitor( - node: interfaces.renderers.TreeNode, - accumulator: Tuple[Dict[str, Dict[str, Any]], List[Dict[str, Any]]], - ) -> Tuple[Dict[str, Dict[str, Any]], List[Dict[str, Any]]]: - # Nodes always have a path value, giving them a path_depth of at least 1, we use max just in case - acc_map, final_tree = accumulator - node_dict: Dict[str, Any] = {"__children": []} - line = [] - for column_index, column in enumerate(grid.columns): - if column in ignore_columns: - continue - - data = list(node.values)[column_index] - - if isinstance(data, interfaces.renderers.BaseAbsentValue): - data = None - - if isinstance(data, renderers.Disassembly): - data = display_disassembly(data) - - if isinstance(data, renderers.LayerData): - data = LayerDataRenderer().render_bytes(data)[0] - - node_dict[column.name] = data - line.append(data) - - if self.filter and self.filter.filter(line): - return accumulator - - if node.parent: - acc_map[node.parent.path]["__children"].append(node_dict) - else: - final_tree.append(node_dict) - acc_map[node.path] = node_dict - - return (acc_map, final_tree) - - if not grid.populated: - grid.populate(visitor, final_output) - else: - grid.visit(node=None, function=visitor, initial_accumulator=final_output) - - schema = self.to_arrow_schema(grid) - self.output_result(schema, outfd, final_output[1]) - - -class ParquetRenderer(ArrowRenderer): - name = "parquet" - structured_output = True - - def get_render_options(self) -> List[interfaces.renderers.RenderOption]: - return [] - - def write_table(self, table: "pa.Table", outfd: TextIO) -> None: - """ - Writes a table to stdout using the Parquet format. - - Args: - t: The Arrow table to write - outfd: The output file descriptor - - Returns: - Nothing - """ - # Write DataFrame to a temporary file-like object - buf = pa.BufferOutputStream() - pq.write_table(table, buf, compression="snappy") - - # Get the buffer as a bytes object - buf_bytes = buf.getvalue().to_pybytes() - outfd.buffer.write(buf_bytes) From 11b312383c5bce78b9bb144a47e9ad47a6954f29 Mon Sep 17 00:00:00 2001 From: Kyrre-Wahl-Kongsgard Date: Tue, 22 Jul 2025 10:09:55 +0200 Subject: [PATCH 34/86] fix: update data type mappings for format_hints.Bin and format_hints.Hex to pa.uint64 --- volatility3/framework/plugins/renderers/parquet_renderer.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/volatility3/framework/plugins/renderers/parquet_renderer.py b/volatility3/framework/plugins/renderers/parquet_renderer.py index c9ef95ee7..3d8f55ecb 100644 --- a/volatility3/framework/plugins/renderers/parquet_renderer.py +++ b/volatility3/framework/plugins/renderers/parquet_renderer.py @@ -53,8 +53,8 @@ class ArrowRenderer(CLIRenderer): float: pa.float64, str: pa.utf8, datetime.datetime: lambda: pa.timestamp("ms"), - format_hints.Bin: pa.int64, - format_hints.Hex: pa.int64, + format_hints.Bin: pa.uint64, + format_hints.Hex: pa.uint64, format_hints.MultiTypeData: pa.utf8, format_hints.HexBytes: pa.binary, renderers.LayerData: pa.binary, From e7185b298f11345e8bbd9e41850e43813627ca03 Mon Sep 17 00:00:00 2001 From: tvanegro Date: Wed, 23 Jul 2025 11:06:20 +0200 Subject: [PATCH 35/86] PR comments --- .../framework/plugins/linux/malware/malfind.py | 7 +------ .../symbols/linux/extensions/__init__.py | 16 +++++++++++++--- 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/volatility3/framework/plugins/linux/malware/malfind.py b/volatility3/framework/plugins/linux/malware/malfind.py index b9a03c616..306bdc3b0 100644 --- a/volatility3/framework/plugins/linux/malware/malfind.py +++ b/volatility3/framework/plugins/linux/malware/malfind.py @@ -61,12 +61,7 @@ class Malfind(interfaces.plugins.PluginInterface): proc_layer = self.context.layers[proc_layer_name] - # Allowing a dump_size of 0 (no dump) - dump_size = ( - self.config.get("dump-size") - if self.config.get("dump-size") is not None - else 64 - ) + dump_size = self.config.get("dump-size", None) or 64 # Dumping page defaults to off, as in case a whole r-xp region is dirty # this would likely dump 1000's of pages which might not always be wise nor necessary diff --git a/volatility3/framework/symbols/linux/extensions/__init__.py b/volatility3/framework/symbols/linux/extensions/__init__.py index 99b2c989c..e48035102 100644 --- a/volatility3/framework/symbols/linux/extensions/__init__.py +++ b/volatility3/framework/symbols/linux/extensions/__init__.py @@ -1273,10 +1273,20 @@ class vm_area_struct(objects.StructType): except exceptions.InvalidAddressException: return None - def get_malicious_pages(self, proclayer=None): - """ - This function will return a list of all malicious pages inside a given dirty region + def get_malicious_pages(self, proclayer) -> List[int]: + """Identifies and returns a list of potentially malicious memory pages. + + A page is considered malicious if it is: + - Executable (protection flags match 'r-x') + - Dirty (modified since process start, according to proclayer.is_dirty()) + + Args: + proclayer: The process's memory layer + + Returns: + List[int]: A list of virtual addresses for pages flagged as potentially malicious. """ + malicious_pages = [] flags_str = self.get_protection() From 533b305afbb3a85a92e9f8466279cac38da4a1ed Mon Sep 17 00:00:00 2001 From: Kyrre-Wahl-Kongsgard Date: Sat, 2 Aug 2025 18:07:05 +0200 Subject: [PATCH 36/86] Added tests for the Parquet and Arrow renderers --- test/renderers/__init__.py | 0 test/renderers/test_parquet_renderers.py | 108 +++++++++++++++++++++++ 2 files changed, 108 insertions(+) create mode 100644 test/renderers/__init__.py create mode 100644 test/renderers/test_parquet_renderers.py diff --git a/test/renderers/__init__.py b/test/renderers/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/test/renderers/test_parquet_renderers.py b/test/renderers/test_parquet_renderers.py new file mode 100644 index 000000000..b45a602aa --- /dev/null +++ b/test/renderers/test_parquet_renderers.py @@ -0,0 +1,108 @@ +import io +import pytest +from abc import ABC, abstractmethod +from test import test_volatility + +HAS_PYARROW = False +try: + import pyarrow as pa + import pyarrow.parquet as pq + import pyarrow.compute as pc + HAS_PYARROW = True +except ImportError: + pass + + +@pytest.mark.skipif(not HAS_PYARROW, reason="pyarrow not installed") +class TestArrowRendererBase(ABC): + """Base class for testing Arrow-based renderers. + + Re-implements Windows and Linux plugin tests using PyArrow operations + instead of text-based assertions. + """ + + renderer_format = None # Override in subclasses + + @abstractmethod + def _get_table_from_output(self, output_bytes) -> "pa.Table": + """Parse output bytes into Arrow table. Override in subclasses.""" + + def test_windows_generic_pslist(self, volatility, python, image): + rc, out, _err = test_volatility.runvol_plugin( + "windows.pslist.PsList", + image, + volatility, + python, + globalargs=("-r", self.renderer_format), + ) + assert rc == 0 + + table = self._get_table_from_output(out) + assert table.num_rows > 10 + + assert table.filter(pc.match_substring(pc.utf8_lower(table.column('ImageFileName')), "system")).num_rows > 0 + assert table.filter(pc.match_substring(pc.utf8_lower(table.column('ImageFileName')), "csrss.exe")).num_rows > 0 + assert table.filter(pc.match_substring(pc.utf8_lower(table.column('ImageFileName')), "svchost.exe")).num_rows > 0 + assert table.filter(pc.greater(table.column('PID'), 0)).num_rows == table.num_rows + + def test_linux_generic_pslist(self, volatility, python, image): + rc, out, _err = test_volatility.runvol_plugin( + "linux.pslist.PsList", + image, + volatility, + python, + globalargs=("-r", self.renderer_format), + ) + assert rc == 0 + + table = self._get_table_from_output(out) + assert table.num_rows > 10 + + init_rows = table.filter(pc.match_substring(pc.utf8_lower(table.column('COMM')), "init")) + systemd_rows = table.filter(pc.match_substring(pc.utf8_lower(table.column('COMM')), "systemd")) + assert (init_rows.num_rows > 0) or (systemd_rows.num_rows > 0) + + assert table.filter(pc.match_substring(pc.utf8_lower(table.column('COMM')), "watchdog")).num_rows > 0 + assert table.filter(pc.greater(table.column('PID'), 0)).num_rows == table.num_rows + + def test_windows_generic_handles(self, volatility, python, image): + rc, out, _err = test_volatility.runvol_plugin( + "windows.handles.Handles", + image, + volatility, + python, + globalargs=("-r", self.renderer_format), + pluginargs=("--pid", "4"), + ) + assert rc == 0 + + table = self._get_table_from_output(out) + assert table.num_rows > 500 + assert table.filter(pc.match_substring(pc.utf8_lower(table.column('Name')), "machine\\system")).num_rows > 0 + + def test_linux_generic_lsof(self, volatility, python, image): + rc, out, _err = test_volatility.runvol_plugin( + "linux.lsof.Lsof", + image, + volatility, + python, + globalargs=("-r", self.renderer_format), + ) + assert rc == 0 + + table = self._get_table_from_output(out) + assert table.num_rows > 35 + +class TestParquetRenderer(TestArrowRendererBase): + renderer_format = "parquet" + + def _get_table_from_output(self, output_bytes): + return pq.read_table(io.BytesIO(output_bytes)) + + +class TestArrowRenderer(TestArrowRendererBase): + renderer_format = "arrow" + + def _get_table_from_output(self, output_bytes): + return pa.ipc.open_stream(io.BytesIO(output_bytes)).read_all() + From eb7daa95a0e19c9022d78361420646d60b706107 Mon Sep 17 00:00:00 2001 From: Kyrre-Wahl-Kongsgard Date: Wed, 6 Aug 2025 15:38:09 +0200 Subject: [PATCH 37/86] flatten tree output --- .../plugins/renderers/parquet_renderer.py | 53 +++++++++++++++++++ 1 file changed, 53 insertions(+) diff --git a/volatility3/framework/plugins/renderers/parquet_renderer.py b/volatility3/framework/plugins/renderers/parquet_renderer.py index 3d8f55ecb..b596349e4 100644 --- a/volatility3/framework/plugins/renderers/parquet_renderer.py +++ b/volatility3/framework/plugins/renderers/parquet_renderer.py @@ -61,6 +61,11 @@ class ArrowRenderer(CLIRenderer): bytes: pa.binary, } + # indicates if the output from the plugin is nested, e.g., pstree + # which would then need to be flattened + self._is_tree_result = False + self._node_id_counter = 0 + def get_render_options(self) -> List[interfaces.renderers.RenderOption]: return [] @@ -69,11 +74,58 @@ class ArrowRenderer(CLIRenderer): for column in grid.columns: arrow_type = self._to_arrow_type[column.type] fields.append(pa.field(column.name, arrow_type())) + + # if the output is nested, e.g., windows.pstree + if self._is_tree_result: + fields.append(pa.field("_vol_id", pa.uint64())) + fields.append(pa.field("_vol_parent_id", pa.uint64())) + return pa.schema(fields) + + def _flatten_tree_structure(self, nested: list[dict]) -> list[dict]: + """ + Flattens a list of nested dicts using the `__children` key. + + Each node gets a `_vol_id` and a `_vol_parent_id` to preserve + the original tree structure in a flat format suitable for tabular output. + + Args: + nested: A list of dicts with optional `__children` lists (tree nodes). + + Returns: + A flat list of dicts with `_vol_id` and `_vol_parent_id`. + """ + rows = [] + self._node_id_counter = 0 + + def _process_node(node: dict, parent_id: int | None): + current_id = self._node_id_counter + self._node_id_counter += 1 + + entry = {k: v for k, v in node.items() if k != "__children"} + entry["_vol_id"] = current_id + entry["_vol_parent_id"] = parent_id + rows.append(entry) + + for child in node.get("__children", []): + _process_node(child, current_id) + + for root in nested: + _process_node(root, None) + + return rows + + + + def output_result(self, schema: "pa.Schema", outfd: TextIO, result): """Outputs the JSON data to a file in a particular format""" + + if self._is_tree_result: + result = self._flatten_tree_structure(result) + t = pa.Table.from_pylist(result, schema=schema) self.write_table(t, outfd) @@ -128,6 +180,7 @@ class ArrowRenderer(CLIRenderer): if node.parent: acc_map[node.parent.path]["__children"].append(node_dict) + self._is_tree_result = True else: final_tree.append(node_dict) acc_map[node.path] = node_dict From 3c830e6e67ab5e2e1743165ef21fc63e13bb3448 Mon Sep 17 00:00:00 2001 From: Kyrre-Wahl-Kongsgard Date: Wed, 6 Aug 2025 15:43:46 +0200 Subject: [PATCH 38/86] black & ruff --- volatility3/framework/plugins/renderers/parquet_renderer.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/volatility3/framework/plugins/renderers/parquet_renderer.py b/volatility3/framework/plugins/renderers/parquet_renderer.py index b596349e4..7593fa4ef 100644 --- a/volatility3/framework/plugins/renderers/parquet_renderer.py +++ b/volatility3/framework/plugins/renderers/parquet_renderer.py @@ -82,7 +82,6 @@ class ArrowRenderer(CLIRenderer): return pa.schema(fields) - def _flatten_tree_structure(self, nested: list[dict]) -> list[dict]: """ Flattens a list of nested dicts using the `__children` key. @@ -116,13 +115,9 @@ class ArrowRenderer(CLIRenderer): return rows - - - def output_result(self, schema: "pa.Schema", outfd: TextIO, result): """Outputs the JSON data to a file in a particular format""" - if self._is_tree_result: result = self._flatten_tree_structure(result) From 47219aad67283653b482eccb69d18b6aa8300748 Mon Sep 17 00:00:00 2001 From: blitztide Date: Fri, 29 Aug 2025 03:06:23 +0100 Subject: [PATCH 39/86] Feature: vadyarascan enrichment This add contextual data to each hit with vadyarascan, saves running pslist after processing. Original didn't have imagename or PPID --- .../framework/plugins/windows/vadyarascan.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/volatility3/framework/plugins/windows/vadyarascan.py b/volatility3/framework/plugins/windows/vadyarascan.py index 04b9aadd9..452a2f774 100644 --- a/volatility3/framework/plugins/windows/vadyarascan.py +++ b/volatility3/framework/plugins/windows/vadyarascan.py @@ -4,6 +4,7 @@ import logging from typing import Iterable, List, Tuple +import datetime from volatility3.framework import interfaces, renderers from volatility3.framework.configuration import requirements @@ -102,6 +103,15 @@ class VadYaraScan(interfaces.plugins.PluginInterface): yield 0, ( format_hints.Hex(offset), task.UniqueProcessId, + task.get_create_time(), + task.InheritedFromUniqueProcessId, + task.ImageFileName.cast( + "string", + max_length=task.ImageFileName.vol.count, + errors="replace", + ), + task.get_session_id(), + task.ActiveThreads, rule_name, name, layer_data, @@ -130,6 +140,11 @@ class VadYaraScan(interfaces.plugins.PluginInterface): [ ("Offset", format_hints.Hex), ("PID", int), + ("CreateTime", datetime.datetime), + ("PPID", int), + ("ImageFileName", str), + ("SessionId", int), + ("Threads", int), ("Rule", str), ("Component", str), ("Value", renderers.LayerData), From 4012887e75457df98c93feb4d44ebb6ac3827fd8 Mon Sep 17 00:00:00 2001 From: Dave Lassalle Date: Thu, 4 Sep 2025 14:50:30 -0500 Subject: [PATCH 40/86] #1780 - cast LoadCount --- .../framework/symbols/windows/extensions/__init__.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/volatility3/framework/symbols/windows/extensions/__init__.py b/volatility3/framework/symbols/windows/extensions/__init__.py index 61138a78c..f32415124 100755 --- a/volatility3/framework/symbols/windows/extensions/__init__.py +++ b/volatility3/framework/symbols/windows/extensions/__init__.py @@ -1715,13 +1715,11 @@ class SHARED_CACHE_MAP(objects.StructType): class LDR_DATA_TABLE_ENTRY(objects.StructType): def get_load_count(self) -> Optional[int]: try: - LoadCount = self.LoadCount + LoadCount = self.LoadCount.cast("short") except Exception: try: - LoadCount = self.ObsoleteLoadCount + LoadCount = self.ObsoleteLoadCount.cast("short") except Exception: LoadCount = None - if LoadCount == 65535: - LoadCount = -1 return LoadCount From d6a791cb675574c6cc35d68cf2e6932f6fe525ce Mon Sep 17 00:00:00 2001 From: blitztide Date: Thu, 4 Sep 2025 22:39:46 +0100 Subject: [PATCH 41/86] VadYaraScan: Increment version --- volatility3/framework/plugins/windows/vadyarascan.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/volatility3/framework/plugins/windows/vadyarascan.py b/volatility3/framework/plugins/windows/vadyarascan.py index 452a2f774..bbbe49f2d 100644 --- a/volatility3/framework/plugins/windows/vadyarascan.py +++ b/volatility3/framework/plugins/windows/vadyarascan.py @@ -19,7 +19,7 @@ class VadYaraScan(interfaces.plugins.PluginInterface): """Scans all the Virtual Address Descriptor memory maps using yara.""" _required_framework_version = (2, 22, 0) - _version = (1, 1, 3) + _version = (1, 1, 4) @classmethod def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]: From d21a81d8c328af72a7bb85edb89eaadf7ec1ff64 Mon Sep 17 00:00:00 2001 From: Kyrre-Wahl-Kongsgard Date: Fri, 5 Sep 2025 06:55:00 +0200 Subject: [PATCH 42/86] fix: update type hints to support Python 3.8 --- .../framework/plugins/renderers/parquet_renderer.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/volatility3/framework/plugins/renderers/parquet_renderer.py b/volatility3/framework/plugins/renderers/parquet_renderer.py index 7593fa4ef..7bc4b82ab 100644 --- a/volatility3/framework/plugins/renderers/parquet_renderer.py +++ b/volatility3/framework/plugins/renderers/parquet_renderer.py @@ -8,6 +8,7 @@ from typing import ( Any, Dict, List, + Optional, Tuple, TextIO, ) @@ -39,7 +40,7 @@ class ArrowRenderer(CLIRenderer): _version = (1, 0, 0) def __init__( - self, options: List[interfaces.renderers.RenderOption] | None = None + self, options: Optional[List[interfaces.renderers.RenderOption]] = None ) -> None: super().__init__(options) @@ -82,7 +83,7 @@ class ArrowRenderer(CLIRenderer): return pa.schema(fields) - def _flatten_tree_structure(self, nested: list[dict]) -> list[dict]: + def _flatten_tree_structure(self, nested: List[Dict]) -> List[Dict]: """ Flattens a list of nested dicts using the `__children` key. @@ -98,7 +99,7 @@ class ArrowRenderer(CLIRenderer): rows = [] self._node_id_counter = 0 - def _process_node(node: dict, parent_id: int | None): + def _process_node(node: Dict, parent_id: Optional[int]): current_id = self._node_id_counter self._node_id_counter += 1 From 88c8bfe1ad30b7e8cdf0cd4aff03a80e119ce5cb Mon Sep 17 00:00:00 2001 From: Dave Lassalle Date: Fri, 5 Sep 2025 13:19:51 -0500 Subject: [PATCH 43/86] #1780 - bump versions --- volatility3/framework/constants/_version.py | 4 ++-- volatility3/framework/plugins/windows/dlllist.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/volatility3/framework/constants/_version.py b/volatility3/framework/constants/_version.py index 07b9e45ec..7f71c277e 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 = 2 # Number of changes that do not change the interface +VERSION_MINOR = 27 # Number of changes that only add to the interface +VERSION_PATCH = 0 # Number of changes that do not change the interface VERSION_SUFFIX = "" PACKAGE_VERSION = ( diff --git a/volatility3/framework/plugins/windows/dlllist.py b/volatility3/framework/plugins/windows/dlllist.py index 1e8ecd414..cb9d3b8bf 100644 --- a/volatility3/framework/plugins/windows/dlllist.py +++ b/volatility3/framework/plugins/windows/dlllist.py @@ -22,7 +22,7 @@ class DllList(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface): """Lists the loaded DLLs in a particular windows memory image.""" _required_framework_version = (2, 0, 0) - _version = (3, 0, 0) + _version = (3, 0, 1) @classmethod def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]: From bec2fb4c7f220daf4ebc0cbad51b3d2cbf914d17 Mon Sep 17 00:00:00 2001 From: j-t-1 <120829237+j-t-1@users.noreply.github.com> Date: Sat, 6 Sep 2025 10:09:28 +0100 Subject: [PATCH 44/86] Add newlines to README.md Purely for readability. --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index cf735fc8d..79401a18f 100644 --- a/README.md +++ b/README.md @@ -66,13 +66,17 @@ pip install -e ".[dev]" Symbol table packs for the various operating systems are available for download at: + + The hashes to verify whether any of the symbol pack files have downloaded successfully or have changed can be found at: + + Symbol tables zip files must be placed, as named, into the `volatility3/symbols` directory (or just the symbols directory next to the executable file). From 86fe4485f0abcb4c10a2f07ae369dfbaa02d1f4a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kyrre=20Wahl=20Kongsg=C3=A5rd?= Date: Mon, 15 Sep 2025 09:31:20 +0200 Subject: [PATCH 45/86] Update volatility3/framework/plugins/renderers/parquet_renderer.py Co-authored-by: ikelos --- volatility3/framework/plugins/renderers/parquet_renderer.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/volatility3/framework/plugins/renderers/parquet_renderer.py b/volatility3/framework/plugins/renderers/parquet_renderer.py index 7bc4b82ab..ff2e07aea 100644 --- a/volatility3/framework/plugins/renderers/parquet_renderer.py +++ b/volatility3/framework/plugins/renderers/parquet_renderer.py @@ -14,11 +14,7 @@ from typing import ( ) from volatility3.framework import interfaces, renderers from volatility3.framework.renderers import format_hints -from volatility3.cli.text_renderer import ( - CLIRenderer, - display_disassembly, - LayerDataRenderer, -) +from volatility3.cli import text_renderer vollog = logging.getLogger(__name__) From 4c7a9b517f45a50e58f5321ad041871ece98386a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kyrre=20Wahl=20Kongsg=C3=A5rd?= Date: Mon, 15 Sep 2025 09:31:28 +0200 Subject: [PATCH 46/86] Update volatility3/framework/plugins/renderers/parquet_renderer.py Co-authored-by: ikelos --- volatility3/framework/plugins/renderers/parquet_renderer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/volatility3/framework/plugins/renderers/parquet_renderer.py b/volatility3/framework/plugins/renderers/parquet_renderer.py index ff2e07aea..197944b85 100644 --- a/volatility3/framework/plugins/renderers/parquet_renderer.py +++ b/volatility3/framework/plugins/renderers/parquet_renderer.py @@ -159,7 +159,7 @@ class ArrowRenderer(CLIRenderer): data = None if isinstance(data, renderers.Disassembly): - data = display_disassembly(data) + data = text_renderer.display_disassembly(data) if isinstance(data, renderers.LayerData): data = LayerDataRenderer().render_bytes(data)[0] From 4af48ae3b0c10911c8da526154da02fff2560373 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kyrre=20Wahl=20Kongsg=C3=A5rd?= Date: Mon, 15 Sep 2025 09:31:34 +0200 Subject: [PATCH 47/86] Update volatility3/framework/plugins/renderers/parquet_renderer.py Co-authored-by: ikelos --- volatility3/framework/plugins/renderers/parquet_renderer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/volatility3/framework/plugins/renderers/parquet_renderer.py b/volatility3/framework/plugins/renderers/parquet_renderer.py index 197944b85..4e8a4c043 100644 --- a/volatility3/framework/plugins/renderers/parquet_renderer.py +++ b/volatility3/framework/plugins/renderers/parquet_renderer.py @@ -28,7 +28,7 @@ except ImportError: vollog.debug("Arrow/Parquet libraries not found") -class ArrowRenderer(CLIRenderer): +class ArrowRenderer(text_renderer.CLIRenderer): """Renderer that outputs Arrow IPC format data.""" name = "arrow" From 8090d72149f7b64c2512ef0370d4dfcd61bbb442 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kyrre=20Wahl=20Kongsg=C3=A5rd?= Date: Mon, 15 Sep 2025 09:32:10 +0200 Subject: [PATCH 48/86] Update volatility3/framework/plugins/renderers/parquet_renderer.py Co-authored-by: ikelos --- volatility3/framework/plugins/renderers/parquet_renderer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/volatility3/framework/plugins/renderers/parquet_renderer.py b/volatility3/framework/plugins/renderers/parquet_renderer.py index 4e8a4c043..f4eaf9c9b 100644 --- a/volatility3/framework/plugins/renderers/parquet_renderer.py +++ b/volatility3/framework/plugins/renderers/parquet_renderer.py @@ -162,7 +162,7 @@ class ArrowRenderer(text_renderer.CLIRenderer): data = text_renderer.display_disassembly(data) if isinstance(data, renderers.LayerData): - data = LayerDataRenderer().render_bytes(data)[0] + data = text_renderer.LayerDataRenderer().render_bytes(data)[0] node_dict[column.name] = data line.append(data) From 81f2e7376ad93a94930de6b11ea99e646229dc22 Mon Sep 17 00:00:00 2001 From: ikelos Date: Mon, 15 Sep 2025 08:58:04 +0100 Subject: [PATCH 49/86] Update test/renderers/test_parquet_renderers.py --- test/renderers/test_parquet_renderers.py | 1 + 1 file changed, 1 insertion(+) diff --git a/test/renderers/test_parquet_renderers.py b/test/renderers/test_parquet_renderers.py index b45a602aa..aa5474636 100644 --- a/test/renderers/test_parquet_renderers.py +++ b/test/renderers/test_parquet_renderers.py @@ -10,6 +10,7 @@ try: import pyarrow.compute as pc HAS_PYARROW = True except ImportError: + # The user doesn't have pyarrow installed, but HAS_PYARROW will be false so just continue pass From 1a1f3be633175b785ca30063e4daa545e8066182 Mon Sep 17 00:00:00 2001 From: Mike Auty Date: Mon, 15 Sep 2025 00:26:58 +0100 Subject: [PATCH 50/86] Swap ObjectInformation from namedtuple to dataclass --- volatility3/framework/interfaces/objects.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/volatility3/framework/interfaces/objects.py b/volatility3/framework/interfaces/objects.py index 2d8024465..e2ba8f380 100644 --- a/volatility3/framework/interfaces/objects.py +++ b/volatility3/framework/interfaces/objects.py @@ -7,6 +7,7 @@ import abc import collections import collections.abc import contextlib +import dataclasses import logging from typing import Any, Dict, List, Mapping, NamedTuple, Optional @@ -52,7 +53,8 @@ class ReadOnlyMapping(collections.abc.Mapping): return dict(self) == dict(other) -class ObjectInformation(NamedTuple): +@dataclasses.dataclass +class ObjectInformation: """Contains common information useful/pertinent only to an individual object (like an instance) @@ -71,12 +73,12 @@ class ObjectInformation(NamedTuple): size: Optional[int] = None def __getitem__(self, key): - if key in self._fields: + if key in self: return getattr(self, key) - raise KeyError(f"NamedTuple does not have a key {key}") + raise KeyError(f"No {key} present in ObjectInformation") def __contains__(self, key): - return key in self._fields + return key in [field.name for field in dataclasses.fields(self)] class ObjectInterface(metaclass=abc.ABCMeta): From 2a2f27a3999090d4f85256016081b5671f7276d1 Mon Sep 17 00:00:00 2001 From: Mike Auty Date: Mon, 15 Sep 2025 00:31:14 +0100 Subject: [PATCH 51/86] 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 e2ba8f380..92ee5f7dc 100644 --- a/volatility3/framework/interfaces/objects.py +++ b/volatility3/framework/interfaces/objects.py @@ -9,7 +9,7 @@ import collections.abc import contextlib import dataclasses import logging -from typing import Any, Dict, List, Mapping, NamedTuple, Optional +from typing import Any, Dict, List, Mapping, Optional from volatility3.framework import constants, interfaces From 207941759dc47d14fc020701fae7053dbda78dac Mon Sep 17 00:00:00 2001 From: SolitudePy <47316655+SolitudePy@users.noreply.github.com> Date: Sun, 1 Jun 2025 19:38:04 +0300 Subject: [PATCH 52/86] pebmasquerade plugin --- .../plugins/windows/pebmasquerade.py | 328 ++++++++++++++++++ 1 file changed, 328 insertions(+) create mode 100644 volatility3/framework/plugins/windows/pebmasquerade.py diff --git a/volatility3/framework/plugins/windows/pebmasquerade.py b/volatility3/framework/plugins/windows/pebmasquerade.py new file mode 100644 index 000000000..dc5293271 --- /dev/null +++ b/volatility3/framework/plugins/windows/pebmasquerade.py @@ -0,0 +1,328 @@ +import logging +import re +from pathlib import PureWindowsPath +from typing import List, Union, Tuple + +from volatility3.framework import interfaces, renderers, exceptions +from volatility3.framework.configuration import requirements +from volatility3.plugins.windows import pslist + +vollog = logging.getLogger(__name__) + + +# https://www.ired.team/offensive-security/defense-evasion/masquerading-processes-in-userland-through-_peb +# https://github.com/FuzzySecurity/PowerShell-Suite/blob/master/Masquerade-PEB.ps1 +class PebMasquerade(interfaces.plugins.PluginInterface): + """Detects potential process name spoofing by comparing EPROCESS and PEB data.""" + + _version = (1, 0, 0) + _required_framework_version = (2, 0, 0) + + @classmethod + def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]: + return [ + requirements.ModuleRequirement( + name="kernel", + description="Windows kernel", + architectures=["Intel32", "Intel64"], + ), + requirements.VersionRequirement( + name="pslist", component=pslist.PsList, version=(3, 0, 0) + ), + requirements.ListRequirement( + name="pid", + element_type=int, + description="Process ID to include (all other processes are excluded)", + optional=True, + ), + ] + + @staticmethod + def _get_cmdline_image(cmdline: str) -> Union[str, PureWindowsPath]: + """Extract the executable path from a command line string. + + Args: + cmdline (str): The command line string to parse. + + Returns: + Union[str, PureWindowsPath]: The executable path as a string or PureWindowsPath. + """ + if not cmdline: + return "" + + # Regex to extract first .exe ending string (handles quotes, paths, no quotes) + match = re.search(r'(?i)(["\']?)([^"\']*?\.exe)\1(?=\s|$)', cmdline) + if match: + exe_path = match.group(2) + return PureWindowsPath(exe_path) + + # If no .exe found, extract the first token (handles quotes) + # Matches either "quoted string" or unquoted word + first_token_match = re.match(r'\s*(?:"([^"]+)"|\'([^\']+)\'|(\S+))', cmdline) + if first_token_match: + # Extract whichever group matched + executable = ( + first_token_match.group(1) + or first_token_match.group(2) + or first_token_match.group(3) + ) + return PureWindowsPath(executable).name + ".exe" + + return "" + + @staticmethod + def _are_paths_equal(device_path: str, drive_path: str) -> Tuple[bool, str, str]: + """Compare two paths to see if they are equal, ignoring drive/device root and case. + + Args: + device_path (str): The device path (e.g. "\\Device\\HarddiskVolume1\\path") + drive_path (str): The drive path (e.g. "C:\\path") + + Returns: + tuple: (are_equal, device_path_without_drive, drive_path_without_drive) + - are_equal (bool): True if paths are equal, False otherwise + - device_path_without_drive (str): Device path without drive letter + - drive_path_without_drive (str): Drive path without drive letter + """ + pure_device_path = PureWindowsPath(device_path) + pure_drive_path = PureWindowsPath(drive_path) + device_parts = list(pure_device_path.parts) + drive_parts = list(pure_drive_path.parts) + + if pure_drive_path.is_absolute(): + new_drive_path = "/".join(drive_parts[1:]).lower() + new_device_path = "/".join(device_parts[3:]).lower() + else: + new_drive_path = "/".join(drive_parts[2:]).lower() + new_device_path = "/".join(device_parts[4:]).lower() + + return ( + new_drive_path == new_device_path, + new_device_path, + new_drive_path, + ) + + def get_process_names(self, proc: interfaces.objects.ObjectInterface) -> Tuple[ + Union[str, renderers.NotAvailableValue], + Union[str, renderers.NotAvailableValue], + Union[str, renderers.NotAvailableValue], + Union[str, renderers.NotAvailableValue], + ]: + """Extract process names and related information from various sources (EPROCESS and PEB). + + Args: + proc: The process object + + Returns: + tuple: (eprocess_imagefilename, eprocess_seaudit_imagefilename, peb_imagefilepath, peb_cmdline) + """ + eprocess_imagefilename = renderers.NotAvailableValue() + eprocess_seaudit_imagefilename = renderers.NotAvailableValue() + peb_imagefilepath = renderers.NotAvailableValue() + peb_cmdline = renderers.NotAvailableValue() + + try: + eprocess_imagefilename = proc.ImageFileName.cast( + "string", + max_length=proc.ImageFileName.vol.count, + errors="replace", + ) + except (AttributeError, exceptions.InvalidAddressException): + vollog.debug( + "Unable to read EPROCESS.ImageFileName for PID %d", proc.UniqueProcessId + ) + except Exception as e: + vollog.warning( + "Error reading EPROCESS.ImageFileName for PID %d: %s", + proc.UniqueProcessId, + str(e)[:50], + ) + + try: + audit = proc.SeAuditProcessCreationInfo.ImageFileName.Name + audit_string = audit.get_string() + if audit_string: + eprocess_seaudit_imagefilename = audit_string + except exceptions.InvalidAddressException: + vollog.debug( + "Unable to read SeAuditProcessCreationInfo.ImageFileName for PID %d", + proc.UniqueProcessId, + ) + except AttributeError: + vollog.debug( + "SeAuditProcessCreationInfo structure not available for PID %d", + proc.UniqueProcessId, + ) + except Exception as e: + vollog.warning( + "Error reading SeAuditProcessCreationInfo for PID %d: %s", + proc.UniqueProcessId, + str(e)[:50], + ) + + try: + peb = proc.get_peb() + if peb and peb.ProcessParameters: + # Get ImagePathName + try: + image_path_str = peb.ProcessParameters.ImagePathName.get_string() + if image_path_str: + peb_imagefilepath = image_path_str + except (AttributeError, exceptions.InvalidAddressException): + vollog.debug( + "Unable to read PEB.ImagePathName for PID %d", + proc.UniqueProcessId, + ) + except Exception as e: + vollog.warning( + "Error reading PEB.ImagePathName for PID %d: %s", + proc.UniqueProcessId, + str(e)[:50], + ) + + try: + cmdline_str = peb.ProcessParameters.CommandLine.get_string() + if cmdline_str: + peb_cmdline = cmdline_str + except (AttributeError, exceptions.InvalidAddressException): + vollog.debug( + "Unable to read PEB.ProcessParameters.CommandLine for PID %d", + proc.UniqueProcessId, + ) + except Exception as e: + vollog.warning( + "Error reading PEB.ProcessParameters.CommandLine for PID %d: %s", + proc.UniqueProcessId, + str(e)[:50], + ) + except (AttributeError, exceptions.InvalidAddressException): + # Important for cases where PEB does not exist or is inaccessible (e.g SYSTEM process) + vollog.debug("Unable to access PEB for PID %d", proc.UniqueProcessId) + except Exception as e: + vollog.warning( + "Error accessing PEB for PID %d: %s", proc.UniqueProcessId, str(e)[:50] + ) + + return ( + eprocess_imagefilename, + eprocess_seaudit_imagefilename, + peb_imagefilepath, + peb_cmdline, + ) + + def _generator(self): + pid_filter = pslist.PsList.create_pid_filter(self.config.get("pid", None)) + + for proc in pslist.PsList.list_processes( + context=self.context, + kernel_module_name=self.config["kernel"], + filter_func=pid_filter, + ): + proc_id = proc.UniqueProcessId + notes = [] + + ( + eprocess_imagefilename, + eprocess_seaudit_imagefilename, + peb_imagefilepath, + peb_cmdline, + ) = self.get_process_names(proc) + proc_name_for_row = eprocess_imagefilename + + # Extract command line executable path for rendering + peb_cmdline_path_render = renderers.NotAvailableValue() + if isinstance(peb_cmdline, str): + try: + peb_cmdline_path_render = str( + PebMasquerade._get_cmdline_image(peb_cmdline) + ) + except Exception as e: + vollog.debug( + "Error extracting command line path for PID %d: %s", + proc_id, + str(e)[:50], + ) + + # Populate notes for enrichment + if isinstance(eprocess_imagefilename, str) and isinstance( + peb_imagefilepath, str + ): + try: + peb_imagefilepath_basename = PureWindowsPath(peb_imagefilepath).name + peb_imagefilepath_truncated = peb_imagefilepath_basename[:14] + + # Compare EPROCESS.ImageFileName with PEB.ImageFilePath truncated to 15 characters + if ( + eprocess_imagefilename.lower() + != peb_imagefilepath_truncated.lower() + ): + notes.append( + f"'Potential PEB.ImageFilePath Spoofing: EPROCESS={eprocess_imagefilename};PEB={peb_imagefilepath_truncated}'" + ) + except Exception as e: + notes.append(f"ImageFilePath Comparison error: {str(e)[:30]}") + + if isinstance(eprocess_imagefilename, str) and isinstance(peb_cmdline, str): + try: + # Compare EPROCESS.ImageFileName with PEB.CommandLine executable path truncated to 15 characters + peb_cmdline_path = PebMasquerade._get_cmdline_image(peb_cmdline) + if isinstance(peb_cmdline_path, PureWindowsPath): + peb_cmdline_path = peb_cmdline_path.name + peb_cmdline_basename_truncated = peb_cmdline_path[:14] + if ( + eprocess_imagefilename.lower() + != peb_cmdline_basename_truncated.lower() + ): + notes.append( + f"'Potential PEB.CommandLine Spoofing: EPROCESS={eprocess_imagefilename};PEB={peb_cmdline_basename_truncated}'" + ) + except Exception as e: + notes.append(f"CommandLine comparison error: {str(e)}") + + if isinstance(eprocess_seaudit_imagefilename, str) and isinstance( + peb_imagefilepath, str + ): + try: + ( + are_equal, + eprocess_seaudit_normalized, + peb_imagefilepath_normalized, + ) = PebMasquerade._are_paths_equal( + device_path=eprocess_seaudit_imagefilename, + drive_path=peb_imagefilepath, + ) + if not are_equal: + notes.append( + f"'Potential PEB.ImageFilePath Spoofing (via _EPROCESS.SeAuditProcessCreationInfo): EPROCESS={eprocess_seaudit_normalized};PEB={peb_imagefilepath_normalized}'" + ) + except Exception as e: + notes.append( + f"SeAuditProcessCreationInfo comparison error: {str(e)[:30]}" + ) + + yield ( + 0, + ( + proc_id, + proc_name_for_row, + eprocess_imagefilename, + eprocess_seaudit_imagefilename, + peb_imagefilepath, + peb_cmdline_path_render, + "[" + ", ".join(notes) + "]" if notes else "OK", + ), + ) + + def run(self): + return renderers.TreeGrid( + [ + ("PID", int), + ("ProcessName", str), + ("EPROCESS_ImageFileName", str), + ("EPROCESS_SeAudit_ImageFileName", str), + ("PEB_ImageFilePath", str), + ("PEB_CommandLine_Path", str), + ("Notes", str), + ], + self._generator(), + ) From 72f0bd1bbafe69cc6695985a13aded63d7edc576 Mon Sep 17 00:00:00 2001 From: SolitudePy <47316655+SolitudePy@users.noreply.github.com> Date: Fri, 6 Jun 2025 17:22:39 +0300 Subject: [PATCH 53/86] UNICODE_STRING length checks to further detect spoofing --- .../plugins/windows/pebmasquerade.py | 46 +++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/volatility3/framework/plugins/windows/pebmasquerade.py b/volatility3/framework/plugins/windows/pebmasquerade.py index dc5293271..c1bfca67f 100644 --- a/volatility3/framework/plugins/windows/pebmasquerade.py +++ b/volatility3/framework/plugins/windows/pebmasquerade.py @@ -219,6 +219,12 @@ class PebMasquerade(interfaces.plugins.PluginInterface): filter_func=pid_filter, ): proc_id = proc.UniqueProcessId + try: + peb = proc.get_peb() + except (exceptions.InvalidAddressException, AttributeError): + vollog.debug( + "Unable to access PEB for PID %d, skipping process", proc_id + ) notes = [] ( @@ -300,6 +306,46 @@ class PebMasquerade(interfaces.plugins.PluginInterface): f"SeAuditProcessCreationInfo comparison error: {str(e)[:30]}" ) + if isinstance(peb_imagefilepath, str) and peb: + try: + + # Length values are of type USHORT + peb_imagefilepath_length = ( + peb.ProcessParameters.ImagePathName.Length // 2 + ) + peb_imagefilepath_maxlength = ( + peb.ProcessParameters.ImagePathName.MaximumLength // 2 - 1 + ) + + if (peb_imagefilepath_length != len(peb_imagefilepath)) or ( + peb_imagefilepath_maxlength != len(peb_imagefilepath) + ): + notes.append( + f"'PEB.ImageFilePath Length Mismatch: Length={peb_imagefilepath_length}, MaximumLength={peb_imagefilepath_maxlength}, Actual={len(peb_imagefilepath)}'" + ) + except Exception as e: + notes.append( + f"PEB.ImageFilePath Length comparison error: {str(e)[:30]}" + ) + + if isinstance(peb_cmdline, str) and peb: + try: + # Length values are of type USHORT + peb_cmdline_length = peb.ProcessParameters.CommandLine.Length // 2 + peb_cmdline_maxlength = ( + peb.ProcessParameters.CommandLine.MaximumLength // 2 - 1 + ) + + if (peb_cmdline_length != len(peb_cmdline)) or ( + peb_cmdline_maxlength != len(peb_cmdline) + ): + notes.append( + f"'PEB.CommandLine Length Mismatch: Commandline={peb_cmdline}, Length={peb_cmdline_length}, MaximumLength={peb_cmdline_maxlength}, Actual={len(peb_cmdline)}'" + ) + except Exception as e: + notes.append( + f"PEB.CommandLine Length comparison error: {str(e)[:30]}" + ) yield ( 0, ( From 231033428df243efe16a60cd2daafef02c4ccfc7 Mon Sep 17 00:00:00 2001 From: SolitudePy <47316655+SolitudePy@users.noreply.github.com> Date: Fri, 6 Jun 2025 18:26:47 +0300 Subject: [PATCH 54/86] replace to utility function --- volatility3/framework/plugins/windows/pebmasquerade.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/volatility3/framework/plugins/windows/pebmasquerade.py b/volatility3/framework/plugins/windows/pebmasquerade.py index c1bfca67f..78676bc0d 100644 --- a/volatility3/framework/plugins/windows/pebmasquerade.py +++ b/volatility3/framework/plugins/windows/pebmasquerade.py @@ -5,6 +5,7 @@ from typing import List, Union, Tuple from volatility3.framework import interfaces, renderers, exceptions from volatility3.framework.configuration import requirements +from volatility3.framework.objects import utility from volatility3.plugins.windows import pslist vollog = logging.getLogger(__name__) @@ -122,11 +123,7 @@ class PebMasquerade(interfaces.plugins.PluginInterface): peb_cmdline = renderers.NotAvailableValue() try: - eprocess_imagefilename = proc.ImageFileName.cast( - "string", - max_length=proc.ImageFileName.vol.count, - errors="replace", - ) + eprocess_imagefilename = utility.array_to_string(proc.ImageFileName) except (AttributeError, exceptions.InvalidAddressException): vollog.debug( "Unable to read EPROCESS.ImageFileName for PID %d", proc.UniqueProcessId From 22ea3d55428cfde26a8ae7c4307b597696e9a9ce Mon Sep 17 00:00:00 2001 From: SolitudePy <47316655+SolitudePy@users.noreply.github.com> Date: Sat, 7 Jun 2025 13:35:27 +0300 Subject: [PATCH 55/86] moved to malware category --- .../framework/plugins/windows/{ => malware}/pebmasquerade.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename volatility3/framework/plugins/windows/{ => malware}/pebmasquerade.py (100%) diff --git a/volatility3/framework/plugins/windows/pebmasquerade.py b/volatility3/framework/plugins/windows/malware/pebmasquerade.py similarity index 100% rename from volatility3/framework/plugins/windows/pebmasquerade.py rename to volatility3/framework/plugins/windows/malware/pebmasquerade.py From 156ca005ded0bc0abc00e9da8ef6e0eca703333c Mon Sep 17 00:00:00 2001 From: SolitudePy <47316655+SolitudePy@users.noreply.github.com> Date: Wed, 11 Jun 2025 21:56:19 +0300 Subject: [PATCH 56/86] changed staticmethods to classmethods --- .../framework/plugins/windows/malware/pebmasquerade.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/volatility3/framework/plugins/windows/malware/pebmasquerade.py b/volatility3/framework/plugins/windows/malware/pebmasquerade.py index 78676bc0d..be54300ff 100644 --- a/volatility3/framework/plugins/windows/malware/pebmasquerade.py +++ b/volatility3/framework/plugins/windows/malware/pebmasquerade.py @@ -38,8 +38,8 @@ class PebMasquerade(interfaces.plugins.PluginInterface): ), ] - @staticmethod - def _get_cmdline_image(cmdline: str) -> Union[str, PureWindowsPath]: + @classmethod + def _get_cmdline_image(cls, cmdline: str) -> Union[str, PureWindowsPath]: """Extract the executable path from a command line string. Args: @@ -71,8 +71,10 @@ class PebMasquerade(interfaces.plugins.PluginInterface): return "" - @staticmethod - def _are_paths_equal(device_path: str, drive_path: str) -> Tuple[bool, str, str]: + @classmethod + def _are_paths_equal( + cls, device_path: str, drive_path: str + ) -> Tuple[bool, str, str]: """Compare two paths to see if they are equal, ignoring drive/device root and case. Args: From 36ce047ec0e1e41a2c2340381a0d62871027e4ed Mon Sep 17 00:00:00 2001 From: SolitudePy <47316655+SolitudePy@users.noreply.github.com> Date: Wed, 11 Jun 2025 22:01:49 +0300 Subject: [PATCH 57/86] PEBMASQ: return None instead of empty string --- volatility3/framework/plugins/windows/malware/pebmasquerade.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/volatility3/framework/plugins/windows/malware/pebmasquerade.py b/volatility3/framework/plugins/windows/malware/pebmasquerade.py index be54300ff..72b138bae 100644 --- a/volatility3/framework/plugins/windows/malware/pebmasquerade.py +++ b/volatility3/framework/plugins/windows/malware/pebmasquerade.py @@ -49,7 +49,7 @@ class PebMasquerade(interfaces.plugins.PluginInterface): Union[str, PureWindowsPath]: The executable path as a string or PureWindowsPath. """ if not cmdline: - return "" + return None # Regex to extract first .exe ending string (handles quotes, paths, no quotes) match = re.search(r'(?i)(["\']?)([^"\']*?\.exe)\1(?=\s|$)', cmdline) From f79a6d6643da54777dce147a8c4af4b1d3094c7b Mon Sep 17 00:00:00 2001 From: SolitudePy <47316655+SolitudePy@users.noreply.github.com> Date: Wed, 11 Jun 2025 22:10:43 +0300 Subject: [PATCH 58/86] PEBMASQ: parameterize _generator --- .../framework/plugins/windows/malware/pebmasquerade.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/volatility3/framework/plugins/windows/malware/pebmasquerade.py b/volatility3/framework/plugins/windows/malware/pebmasquerade.py index 72b138bae..2e1e47b39 100644 --- a/volatility3/framework/plugins/windows/malware/pebmasquerade.py +++ b/volatility3/framework/plugins/windows/malware/pebmasquerade.py @@ -209,8 +209,8 @@ class PebMasquerade(interfaces.plugins.PluginInterface): peb_cmdline, ) - def _generator(self): - pid_filter = pslist.PsList.create_pid_filter(self.config.get("pid", None)) + def _generator(self, pids): + pid_filter = pslist.PsList.create_pid_filter(pids) for proc in pslist.PsList.list_processes( context=self.context, @@ -359,6 +359,7 @@ class PebMasquerade(interfaces.plugins.PluginInterface): ) def run(self): + pids = self.config.get("pid", None) return renderers.TreeGrid( [ ("PID", int), @@ -369,5 +370,5 @@ class PebMasquerade(interfaces.plugins.PluginInterface): ("PEB_CommandLine_Path", str), ("Notes", str), ], - self._generator(), + self._generator(pids), ) From bcc6ce68c51a4ed6fd287813ddd84b5e800152f9 Mon Sep 17 00:00:00 2001 From: SolitudePy <47316655+SolitudePy@users.noreply.github.com> Date: Wed, 11 Jun 2025 22:13:52 +0300 Subject: [PATCH 59/86] PEBMASQ: parameterize _generator v2 --- .../framework/plugins/windows/malware/pebmasquerade.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/volatility3/framework/plugins/windows/malware/pebmasquerade.py b/volatility3/framework/plugins/windows/malware/pebmasquerade.py index 2e1e47b39..e4bd219e2 100644 --- a/volatility3/framework/plugins/windows/malware/pebmasquerade.py +++ b/volatility3/framework/plugins/windows/malware/pebmasquerade.py @@ -209,12 +209,12 @@ class PebMasquerade(interfaces.plugins.PluginInterface): peb_cmdline, ) - def _generator(self, pids): + def _generator(self, pids, context, kernel_module_name): pid_filter = pslist.PsList.create_pid_filter(pids) for proc in pslist.PsList.list_processes( - context=self.context, - kernel_module_name=self.config["kernel"], + context=context, + kernel_module_name=kernel_module_name, filter_func=pid_filter, ): proc_id = proc.UniqueProcessId @@ -360,6 +360,8 @@ class PebMasquerade(interfaces.plugins.PluginInterface): def run(self): pids = self.config.get("pid", None) + context = self.context + kernel_module_name = self.config["kernel"] return renderers.TreeGrid( [ ("PID", int), @@ -370,5 +372,5 @@ class PebMasquerade(interfaces.plugins.PluginInterface): ("PEB_CommandLine_Path", str), ("Notes", str), ], - self._generator(pids), + self._generator(pids, context, kernel_module_name), ) From 4adc3305a3883f33d3751ed4f6c21cf5fbcf5366 Mon Sep 17 00:00:00 2001 From: SolitudePy <47316655+SolitudePy@users.noreply.github.com> Date: Thu, 12 Jun 2025 20:04:49 +0300 Subject: [PATCH 60/86] remove error truncate --- .../plugins/windows/malware/pebmasquerade.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/volatility3/framework/plugins/windows/malware/pebmasquerade.py b/volatility3/framework/plugins/windows/malware/pebmasquerade.py index e4bd219e2..8c6c0b4b0 100644 --- a/volatility3/framework/plugins/windows/malware/pebmasquerade.py +++ b/volatility3/framework/plugins/windows/malware/pebmasquerade.py @@ -134,7 +134,7 @@ class PebMasquerade(interfaces.plugins.PluginInterface): vollog.warning( "Error reading EPROCESS.ImageFileName for PID %d: %s", proc.UniqueProcessId, - str(e)[:50], + str(e), ) try: @@ -156,7 +156,7 @@ class PebMasquerade(interfaces.plugins.PluginInterface): vollog.warning( "Error reading SeAuditProcessCreationInfo for PID %d: %s", proc.UniqueProcessId, - str(e)[:50], + str(e), ) try: @@ -176,7 +176,7 @@ class PebMasquerade(interfaces.plugins.PluginInterface): vollog.warning( "Error reading PEB.ImagePathName for PID %d: %s", proc.UniqueProcessId, - str(e)[:50], + str(e), ) try: @@ -192,14 +192,14 @@ class PebMasquerade(interfaces.plugins.PluginInterface): vollog.warning( "Error reading PEB.ProcessParameters.CommandLine for PID %d: %s", proc.UniqueProcessId, - str(e)[:50], + str(e), ) except (AttributeError, exceptions.InvalidAddressException): # Important for cases where PEB does not exist or is inaccessible (e.g SYSTEM process) vollog.debug("Unable to access PEB for PID %d", proc.UniqueProcessId) except Exception as e: vollog.warning( - "Error accessing PEB for PID %d: %s", proc.UniqueProcessId, str(e)[:50] + "Error accessing PEB for PID %d: %s", proc.UniqueProcessId, str(e) ) return ( @@ -245,7 +245,7 @@ class PebMasquerade(interfaces.plugins.PluginInterface): vollog.debug( "Error extracting command line path for PID %d: %s", proc_id, - str(e)[:50], + str(e), ) # Populate notes for enrichment From 32def71eb5a42d44709a2a1477194c8f2f61215a Mon Sep 17 00:00:00 2001 From: SolitudePy <47316655+SolitudePy@users.noreply.github.com> Date: Tue, 22 Jul 2025 21:34:55 +0300 Subject: [PATCH 61/86] remove notes --- .../plugins/windows/malware/pebmasquerade.py | 86 ++++--------------- 1 file changed, 16 insertions(+), 70 deletions(-) diff --git a/volatility3/framework/plugins/windows/malware/pebmasquerade.py b/volatility3/framework/plugins/windows/malware/pebmasquerade.py index 8c6c0b4b0..cc3ee73bc 100644 --- a/volatility3/framework/plugins/windows/malware/pebmasquerade.py +++ b/volatility3/framework/plugins/windows/malware/pebmasquerade.py @@ -225,14 +225,14 @@ class PebMasquerade(interfaces.plugins.PluginInterface): "Unable to access PEB for PID %d, skipping process", proc_id ) notes = [] - + peb_imagefilepath_length_check = False + peb_cmdline_length_check = False ( eprocess_imagefilename, eprocess_seaudit_imagefilename, peb_imagefilepath, peb_cmdline, ) = self.get_process_names(proc) - proc_name_for_row = eprocess_imagefilename # Extract command line executable path for rendering peb_cmdline_path_render = renderers.NotAvailableValue() @@ -248,63 +248,6 @@ class PebMasquerade(interfaces.plugins.PluginInterface): str(e), ) - # Populate notes for enrichment - if isinstance(eprocess_imagefilename, str) and isinstance( - peb_imagefilepath, str - ): - try: - peb_imagefilepath_basename = PureWindowsPath(peb_imagefilepath).name - peb_imagefilepath_truncated = peb_imagefilepath_basename[:14] - - # Compare EPROCESS.ImageFileName with PEB.ImageFilePath truncated to 15 characters - if ( - eprocess_imagefilename.lower() - != peb_imagefilepath_truncated.lower() - ): - notes.append( - f"'Potential PEB.ImageFilePath Spoofing: EPROCESS={eprocess_imagefilename};PEB={peb_imagefilepath_truncated}'" - ) - except Exception as e: - notes.append(f"ImageFilePath Comparison error: {str(e)[:30]}") - - if isinstance(eprocess_imagefilename, str) and isinstance(peb_cmdline, str): - try: - # Compare EPROCESS.ImageFileName with PEB.CommandLine executable path truncated to 15 characters - peb_cmdline_path = PebMasquerade._get_cmdline_image(peb_cmdline) - if isinstance(peb_cmdline_path, PureWindowsPath): - peb_cmdline_path = peb_cmdline_path.name - peb_cmdline_basename_truncated = peb_cmdline_path[:14] - if ( - eprocess_imagefilename.lower() - != peb_cmdline_basename_truncated.lower() - ): - notes.append( - f"'Potential PEB.CommandLine Spoofing: EPROCESS={eprocess_imagefilename};PEB={peb_cmdline_basename_truncated}'" - ) - except Exception as e: - notes.append(f"CommandLine comparison error: {str(e)}") - - if isinstance(eprocess_seaudit_imagefilename, str) and isinstance( - peb_imagefilepath, str - ): - try: - ( - are_equal, - eprocess_seaudit_normalized, - peb_imagefilepath_normalized, - ) = PebMasquerade._are_paths_equal( - device_path=eprocess_seaudit_imagefilename, - drive_path=peb_imagefilepath, - ) - if not are_equal: - notes.append( - f"'Potential PEB.ImageFilePath Spoofing (via _EPROCESS.SeAuditProcessCreationInfo): EPROCESS={eprocess_seaudit_normalized};PEB={peb_imagefilepath_normalized}'" - ) - except Exception as e: - notes.append( - f"SeAuditProcessCreationInfo comparison error: {str(e)[:30]}" - ) - if isinstance(peb_imagefilepath, str) and peb: try: @@ -319,12 +262,12 @@ class PebMasquerade(interfaces.plugins.PluginInterface): if (peb_imagefilepath_length != len(peb_imagefilepath)) or ( peb_imagefilepath_maxlength != len(peb_imagefilepath) ): - notes.append( - f"'PEB.ImageFilePath Length Mismatch: Length={peb_imagefilepath_length}, MaximumLength={peb_imagefilepath_maxlength}, Actual={len(peb_imagefilepath)}'" - ) + peb_imagefilepath_length_check = True except Exception as e: - notes.append( - f"PEB.ImageFilePath Length comparison error: {str(e)[:30]}" + vollog.warning( + "PEB.ImagePathName Length comparison error for PID %d: %s", + proc_id, + str(e), ) if isinstance(peb_cmdline, str) and peb: @@ -338,23 +281,26 @@ class PebMasquerade(interfaces.plugins.PluginInterface): if (peb_cmdline_length != len(peb_cmdline)) or ( peb_cmdline_maxlength != len(peb_cmdline) ): + peb_cmdline_length_check = True notes.append( f"'PEB.CommandLine Length Mismatch: Commandline={peb_cmdline}, Length={peb_cmdline_length}, MaximumLength={peb_cmdline_maxlength}, Actual={len(peb_cmdline)}'" ) except Exception as e: - notes.append( - f"PEB.CommandLine Length comparison error: {str(e)[:30]}" + vollog.warning( + "PEB.CommandLine Length comparison error for PID %d: %s", + proc_id, + str(e), ) yield ( 0, ( proc_id, - proc_name_for_row, eprocess_imagefilename, eprocess_seaudit_imagefilename, peb_imagefilepath, peb_cmdline_path_render, - "[" + ", ".join(notes) + "]" if notes else "OK", + peb_cmdline_length_check, + peb_imagefilepath_length_check, ), ) @@ -365,12 +311,12 @@ class PebMasquerade(interfaces.plugins.PluginInterface): return renderers.TreeGrid( [ ("PID", int), - ("ProcessName", str), ("EPROCESS_ImageFileName", str), ("EPROCESS_SeAudit_ImageFileName", str), ("PEB_ImageFilePath", str), ("PEB_CommandLine_Path", str), - ("Notes", str), + ("PEB_ImageFilePath_Spoofed", bool), + ("PEB_CommandLine_Spoofed", bool), ], self._generator(pids, context, kernel_module_name), ) From 035b60863308afd31d0d6ee543d296e856c54a13 Mon Sep 17 00:00:00 2001 From: SolitudePy <47316655+SolitudePy@users.noreply.github.com> Date: Tue, 22 Jul 2025 21:49:16 +0300 Subject: [PATCH 62/86] Plugins: pebmasquerade remove unused code --- .../plugins/windows/malware/pebmasquerade.py | 94 +------------------ 1 file changed, 3 insertions(+), 91 deletions(-) diff --git a/volatility3/framework/plugins/windows/malware/pebmasquerade.py b/volatility3/framework/plugins/windows/malware/pebmasquerade.py index cc3ee73bc..070716503 100644 --- a/volatility3/framework/plugins/windows/malware/pebmasquerade.py +++ b/volatility3/framework/plugins/windows/malware/pebmasquerade.py @@ -1,6 +1,4 @@ import logging -import re -from pathlib import PureWindowsPath from typing import List, Union, Tuple from volatility3.framework import interfaces, renderers, exceptions @@ -38,74 +36,8 @@ class PebMasquerade(interfaces.plugins.PluginInterface): ), ] - @classmethod - def _get_cmdline_image(cls, cmdline: str) -> Union[str, PureWindowsPath]: - """Extract the executable path from a command line string. - - Args: - cmdline (str): The command line string to parse. - - Returns: - Union[str, PureWindowsPath]: The executable path as a string or PureWindowsPath. - """ - if not cmdline: - return None - - # Regex to extract first .exe ending string (handles quotes, paths, no quotes) - match = re.search(r'(?i)(["\']?)([^"\']*?\.exe)\1(?=\s|$)', cmdline) - if match: - exe_path = match.group(2) - return PureWindowsPath(exe_path) - - # If no .exe found, extract the first token (handles quotes) - # Matches either "quoted string" or unquoted word - first_token_match = re.match(r'\s*(?:"([^"]+)"|\'([^\']+)\'|(\S+))', cmdline) - if first_token_match: - # Extract whichever group matched - executable = ( - first_token_match.group(1) - or first_token_match.group(2) - or first_token_match.group(3) - ) - return PureWindowsPath(executable).name + ".exe" - - return "" - - @classmethod - def _are_paths_equal( - cls, device_path: str, drive_path: str - ) -> Tuple[bool, str, str]: - """Compare two paths to see if they are equal, ignoring drive/device root and case. - - Args: - device_path (str): The device path (e.g. "\\Device\\HarddiskVolume1\\path") - drive_path (str): The drive path (e.g. "C:\\path") - - Returns: - tuple: (are_equal, device_path_without_drive, drive_path_without_drive) - - are_equal (bool): True if paths are equal, False otherwise - - device_path_without_drive (str): Device path without drive letter - - drive_path_without_drive (str): Drive path without drive letter - """ - pure_device_path = PureWindowsPath(device_path) - pure_drive_path = PureWindowsPath(drive_path) - device_parts = list(pure_device_path.parts) - drive_parts = list(pure_drive_path.parts) - - if pure_drive_path.is_absolute(): - new_drive_path = "/".join(drive_parts[1:]).lower() - new_device_path = "/".join(device_parts[3:]).lower() - else: - new_drive_path = "/".join(drive_parts[2:]).lower() - new_device_path = "/".join(device_parts[4:]).lower() - - return ( - new_drive_path == new_device_path, - new_device_path, - new_drive_path, - ) - - def get_process_names(self, proc: interfaces.objects.ObjectInterface) -> Tuple[ + @staticmethod + def get_process_names(proc: interfaces.objects.ObjectInterface) -> Tuple[ Union[str, renderers.NotAvailableValue], Union[str, renderers.NotAvailableValue], Union[str, renderers.NotAvailableValue], @@ -224,7 +156,6 @@ class PebMasquerade(interfaces.plugins.PluginInterface): vollog.debug( "Unable to access PEB for PID %d, skipping process", proc_id ) - notes = [] peb_imagefilepath_length_check = False peb_cmdline_length_check = False ( @@ -232,21 +163,7 @@ class PebMasquerade(interfaces.plugins.PluginInterface): eprocess_seaudit_imagefilename, peb_imagefilepath, peb_cmdline, - ) = self.get_process_names(proc) - - # Extract command line executable path for rendering - peb_cmdline_path_render = renderers.NotAvailableValue() - if isinstance(peb_cmdline, str): - try: - peb_cmdline_path_render = str( - PebMasquerade._get_cmdline_image(peb_cmdline) - ) - except Exception as e: - vollog.debug( - "Error extracting command line path for PID %d: %s", - proc_id, - str(e), - ) + ) = PebMasquerade.get_process_names(proc) if isinstance(peb_imagefilepath, str) and peb: try: @@ -282,9 +199,6 @@ class PebMasquerade(interfaces.plugins.PluginInterface): peb_cmdline_maxlength != len(peb_cmdline) ): peb_cmdline_length_check = True - notes.append( - f"'PEB.CommandLine Length Mismatch: Commandline={peb_cmdline}, Length={peb_cmdline_length}, MaximumLength={peb_cmdline_maxlength}, Actual={len(peb_cmdline)}'" - ) except Exception as e: vollog.warning( "PEB.CommandLine Length comparison error for PID %d: %s", @@ -298,7 +212,6 @@ class PebMasquerade(interfaces.plugins.PluginInterface): eprocess_imagefilename, eprocess_seaudit_imagefilename, peb_imagefilepath, - peb_cmdline_path_render, peb_cmdline_length_check, peb_imagefilepath_length_check, ), @@ -314,7 +227,6 @@ class PebMasquerade(interfaces.plugins.PluginInterface): ("EPROCESS_ImageFileName", str), ("EPROCESS_SeAudit_ImageFileName", str), ("PEB_ImageFilePath", str), - ("PEB_CommandLine_Path", str), ("PEB_ImageFilePath_Spoofed", bool), ("PEB_CommandLine_Spoofed", bool), ], From 2dd0bec92744d376fb649d7b1d759c5282db2cda Mon Sep 17 00:00:00 2001 From: SolitudePy <47316655+SolitudePy@users.noreply.github.com> Date: Wed, 17 Sep 2025 02:03:48 +0300 Subject: [PATCH 63/86] Plugins: pebmasq - change staticmethod to classmethod --- volatility3/framework/plugins/windows/malware/pebmasquerade.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/volatility3/framework/plugins/windows/malware/pebmasquerade.py b/volatility3/framework/plugins/windows/malware/pebmasquerade.py index 070716503..c2636820b 100644 --- a/volatility3/framework/plugins/windows/malware/pebmasquerade.py +++ b/volatility3/framework/plugins/windows/malware/pebmasquerade.py @@ -36,7 +36,7 @@ class PebMasquerade(interfaces.plugins.PluginInterface): ), ] - @staticmethod + @classmethod def get_process_names(proc: interfaces.objects.ObjectInterface) -> Tuple[ Union[str, renderers.NotAvailableValue], Union[str, renderers.NotAvailableValue], From a6d1b34d36ab8a3d63dc3d1f1f07bf965e9712dc Mon Sep 17 00:00:00 2001 From: SolitudePy <47316655+SolitudePy@users.noreply.github.com> Date: Wed, 17 Sep 2025 13:46:34 +0300 Subject: [PATCH 64/86] Plugins: pebmasq fix classmethod --- volatility3/framework/plugins/windows/malware/pebmasquerade.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/volatility3/framework/plugins/windows/malware/pebmasquerade.py b/volatility3/framework/plugins/windows/malware/pebmasquerade.py index c2636820b..3d662789a 100644 --- a/volatility3/framework/plugins/windows/malware/pebmasquerade.py +++ b/volatility3/framework/plugins/windows/malware/pebmasquerade.py @@ -37,7 +37,7 @@ class PebMasquerade(interfaces.plugins.PluginInterface): ] @classmethod - def get_process_names(proc: interfaces.objects.ObjectInterface) -> Tuple[ + def get_process_names(cls, proc: interfaces.objects.ObjectInterface) -> Tuple[ Union[str, renderers.NotAvailableValue], Union[str, renderers.NotAvailableValue], Union[str, renderers.NotAvailableValue], From 0e17057b0b6a01425cf3f4c66c42233e7ccb7f83 Mon Sep 17 00:00:00 2001 From: SolitudePy <47316655+SolitudePy@users.noreply.github.com> Date: Tue, 23 Sep 2025 21:16:47 +0300 Subject: [PATCH 65/86] Plugins - pebmasq required framework version --- volatility3/framework/plugins/windows/malware/pebmasquerade.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/volatility3/framework/plugins/windows/malware/pebmasquerade.py b/volatility3/framework/plugins/windows/malware/pebmasquerade.py index 3d662789a..dd85fb570 100644 --- a/volatility3/framework/plugins/windows/malware/pebmasquerade.py +++ b/volatility3/framework/plugins/windows/malware/pebmasquerade.py @@ -15,7 +15,7 @@ class PebMasquerade(interfaces.plugins.PluginInterface): """Detects potential process name spoofing by comparing EPROCESS and PEB data.""" _version = (1, 0, 0) - _required_framework_version = (2, 0, 0) + _required_framework_version = (2, 27, 0) @classmethod def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]: From 438acddab635553b9820e8bdc6815a59ed99b222 Mon Sep 17 00:00:00 2001 From: ikelos Date: Sun, 28 Sep 2025 22:11:26 +0100 Subject: [PATCH 66/86] Update volatility3/framework/plugins/linux/malware/malfind.py --- volatility3/framework/plugins/linux/malware/malfind.py | 1 + 1 file changed, 1 insertion(+) diff --git a/volatility3/framework/plugins/linux/malware/malfind.py b/volatility3/framework/plugins/linux/malware/malfind.py index 306bdc3b0..721e1953d 100644 --- a/volatility3/framework/plugins/linux/malware/malfind.py +++ b/volatility3/framework/plugins/linux/malware/malfind.py @@ -41,6 +41,7 @@ class Malfind(interfaces.plugins.PluginInterface): name="dump-size", description="Amount of bytes to dump for each dirty region/page found - Default 64 bytes", optional=True, + default=64, ), requirements.BooleanRequirement( name="dump-page", From 16412537c0065ba284d83e5dc41d6b1d01a37a43 Mon Sep 17 00:00:00 2001 From: ikelos Date: Sun, 28 Sep 2025 22:11:33 +0100 Subject: [PATCH 67/86] Update volatility3/framework/plugins/linux/malware/malfind.py --- volatility3/framework/plugins/linux/malware/malfind.py | 1 + 1 file changed, 1 insertion(+) diff --git a/volatility3/framework/plugins/linux/malware/malfind.py b/volatility3/framework/plugins/linux/malware/malfind.py index 721e1953d..09b64768f 100644 --- a/volatility3/framework/plugins/linux/malware/malfind.py +++ b/volatility3/framework/plugins/linux/malware/malfind.py @@ -47,6 +47,7 @@ class Malfind(interfaces.plugins.PluginInterface): name="dump-page", description="Dump each dirty page and content - Default off", optional=True, + default=False, ), ] From d41afc444e7590b83dd9f14702f76ed094371856 Mon Sep 17 00:00:00 2001 From: ikelos Date: Sun, 28 Sep 2025 22:11:40 +0100 Subject: [PATCH 68/86] Update volatility3/framework/plugins/linux/malware/malfind.py --- volatility3/framework/plugins/linux/malware/malfind.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/volatility3/framework/plugins/linux/malware/malfind.py b/volatility3/framework/plugins/linux/malware/malfind.py index 09b64768f..10fa71d36 100644 --- a/volatility3/framework/plugins/linux/malware/malfind.py +++ b/volatility3/framework/plugins/linux/malware/malfind.py @@ -63,7 +63,7 @@ class Malfind(interfaces.plugins.PluginInterface): proc_layer = self.context.layers[proc_layer_name] - dump_size = self.config.get("dump-size", None) or 64 + dump_size = self.config["dump-size"] # Dumping page defaults to off, as in case a whole r-xp region is dirty # this would likely dump 1000's of pages which might not always be wise nor necessary From f72b8ee21d72751907a16ea66cba692fe5c78790 Mon Sep 17 00:00:00 2001 From: ikelos Date: Sun, 28 Sep 2025 22:11:48 +0100 Subject: [PATCH 69/86] Update volatility3/framework/plugins/linux/malware/malfind.py --- volatility3/framework/plugins/linux/malware/malfind.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/volatility3/framework/plugins/linux/malware/malfind.py b/volatility3/framework/plugins/linux/malware/malfind.py index 10fa71d36..cbd9f87c1 100644 --- a/volatility3/framework/plugins/linux/malware/malfind.py +++ b/volatility3/framework/plugins/linux/malware/malfind.py @@ -68,7 +68,7 @@ class Malfind(interfaces.plugins.PluginInterface): # Dumping page defaults to off, as in case a whole r-xp region is dirty # this would likely dump 1000's of pages which might not always be wise nor necessary - dump_page = self.config.get("dump-page") or False + dump_page = self.config["dump-page"] for vma in task.mm.get_vma_iter(): vma_name = vma.get_name(self.context, task) From 841b2b6435911221c09ce4028a541d081ea621a8 Mon Sep 17 00:00:00 2001 From: Mike Auty Date: Mon, 29 Sep 2025 23:04:01 +0100 Subject: [PATCH 70/86] Ruff fixes that black agrees with --- development/banner_server.py | 2 - development/compare-vol.py | 1 - development/pdbparse-to-json.py | 1 - development/stock-linux-json.py | 1 - test/plugins/windows/test_scheduled_tasks.py | 2036 +++++++++++++++-- test/renderers/test_parquet_renderers.py | 64 +- test/volatility3_code_analysis.py | 10 +- volatility3/__init__.py | 1 + volatility3/cli/__init__.py | 10 +- volatility3/cli/text_renderer.py | 2 - volatility3/cli/volshell/__init__.py | 5 +- volatility3/cli/volshell/generic.py | 6 +- volatility3/framework/__init__.py | 1 + volatility3/framework/automagic/pdbscan.py | 1 + volatility3/framework/automagic/stacker.py | 5 +- volatility3/framework/automagic/windows.py | 1 + .../framework/configuration/requirements.py | 1 + .../framework/constants/linux/__init__.py | 1 + volatility3/framework/contexts/__init__.py | 1 + volatility3/framework/deprecation.py | 2 +- volatility3/framework/exceptions.py | 3 +- volatility3/framework/interfaces/automagic.py | 1 + volatility3/framework/interfaces/context.py | 1 + volatility3/framework/interfaces/layers.py | 1 + volatility3/framework/interfaces/objects.py | 1 + volatility3/framework/interfaces/symbols.py | 1 + volatility3/framework/layers/avml.py | 1 + .../framework/layers/codecs/__init__.py | 5 +- volatility3/framework/layers/intel.py | 16 +- volatility3/framework/layers/msf.py | 10 +- volatility3/framework/layers/resources.py | 5 +- volatility3/framework/layers/segmented.py | 8 +- volatility3/framework/layers/vmware.py | 8 +- volatility3/framework/plugins/banners.py | 5 +- volatility3/framework/plugins/isfinfo.py | 9 +- .../framework/plugins/linux/graphics/fbdev.py | 1 - volatility3/framework/plugins/linux/ip.py | 58 +- volatility3/framework/plugins/linux/kmsg.py | 15 +- volatility3/framework/plugins/linux/lsof.py | 6 +- .../plugins/linux/malware/check_afinfo.py | 1 + .../plugins/linux/malware/check_syscall.py | 1 + .../plugins/linux/malware/netfilter.py | 11 +- .../plugins/linux/malware/tty_check.py | 13 +- .../framework/plugins/linux/module_extract.py | 11 +- .../framework/plugins/linux/pagecache.py | 7 +- volatility3/framework/plugins/linux/pslist.py | 27 +- .../plugins/linux/tracing/perf_events.py | 1 - .../framework/plugins/linux/vmaregexscan.py | 16 +- .../framework/plugins/linux/vmayarascan.py | 15 +- volatility3/framework/plugins/mac/lsmod.py | 1 + volatility3/framework/plugins/mac/mount.py | 1 + volatility3/framework/plugins/mac/psaux.py | 1 + .../framework/plugins/windows/callbacks.py | 7 +- .../framework/plugins/windows/deskscan.py | 11 +- .../framework/plugins/windows/desktops.py | 11 +- .../framework/plugins/windows/kpcrs.py | 1 - .../windows/malware/direct_system_calls.py | 15 +- .../windows/malware/hollowprocesses.py | 11 +- .../plugins/windows/malware/malfind.py | 7 +- .../plugins/windows/malware/pebmasquerade.py | 1 - .../windows/malware/processghosting.py | 28 +- .../windows/malware/skeleton_key_check.py | 15 +- .../windows/malware/suspicious_threads.py | 19 +- .../framework/plugins/windows/mftscan.py | 140 +- .../framework/plugins/windows/modules.py | 17 +- .../framework/plugins/windows/poolscanner.py | 1 - .../plugins/windows/registry/amcache.py | 120 +- .../windows/registry/scheduled_tasks.py | 22 +- .../framework/plugins/windows/sessions.py | 17 +- .../framework/plugins/windows/shimcachemem.py | 14 +- .../framework/plugins/windows/svcscan.py | 1 - .../framework/plugins/windows/thrdscan.py | 23 +- .../framework/plugins/windows/vadregexscan.py | 16 +- .../framework/plugins/windows/vadyarascan.py | 31 +- .../framework/plugins/windows/windows.py | 21 +- volatility3/framework/renderers/__init__.py | 1 + .../framework/renderers/format_hints.py | 1 + volatility3/framework/symbols/__init__.py | 7 +- volatility3/framework/symbols/intermed.py | 9 +- .../symbols/linux/extensions/__init__.py | 3 - .../symbols/linux/utilities/modules.py | 18 +- .../symbols/windows/extensions/callbacks.py | 1 - .../symbols/windows/extensions/gui.py | 1 - .../symbols/windows/extensions/mft.py | 1 - .../symbols/windows/extensions/shimcache.py | 1 - .../framework/symbols/windows/pdbconv.py | 2 +- volatility3/plugins/__init__.py | 1 + volatility3/plugins/linux/__init__.py | 1 + volatility3/plugins/mac/__init__.py | 1 + volatility3/plugins/windows/__init__.py | 1 + .../plugins/windows/registry/__init__.py | 1 + volatility3/symbols/__init__.py | 1 + 92 files changed, 2425 insertions(+), 588 deletions(-) diff --git a/development/banner_server.py b/development/banner_server.py index b62477a26..dd1df96ab 100644 --- a/development/banner_server.py +++ b/development/banner_server.py @@ -14,7 +14,6 @@ vollog = logging.getLogger(__name__) class BannerCacheGenerator: - def __init__(self, path: str, url_prefix: str): self._path = path self._url_prefix = url_prefix @@ -79,7 +78,6 @@ class BannerCacheGenerator: if __name__ == "__main__": - parser = argparse.ArgumentParser() parser.add_argument("--path", default=os.path.dirname(__file__)) parser.add_argument( diff --git a/development/compare-vol.py b/development/compare-vol.py index 717d81d3e..ccfcd9f4a 100644 --- a/development/compare-vol.py +++ b/development/compare-vol.py @@ -208,7 +208,6 @@ class Volatility3PyPyTest(VolatilityTest): class VolatilityTester: - def __init__( self, images: List[VolatilityImage], diff --git a/development/pdbparse-to-json.py b/development/pdbparse-to-json.py index 49b4da009..fe4b8ab60 100644 --- a/development/pdbparse-to-json.py +++ b/development/pdbparse-to-json.py @@ -22,7 +22,6 @@ if __name__ == "__main__": class PDBRetreiver: - def retreive_pdb(self, guid: str, file_name: str) -> Optional[str]: logger.info("Download PDB file...") file_name = ".".join(file_name.split(".")[:-1] + ["pdb"]) diff --git a/development/stock-linux-json.py b/development/stock-linux-json.py index 967cc7e18..713283e66 100644 --- a/development/stock-linux-json.py +++ b/development/stock-linux-json.py @@ -13,7 +13,6 @@ DWARF2JSON = "./dwarf2json" class Downloader: - def __init__(self, url_lists: List[List[str]]) -> None: self.url_lists = url_lists diff --git a/test/plugins/windows/test_scheduled_tasks.py b/test/plugins/windows/test_scheduled_tasks.py index 15d7f79a6..ce66ee23e 100644 --- a/test/plugins/windows/test_scheduled_tasks.py +++ b/test/plugins/windows/test_scheduled_tasks.py @@ -101,233 +101,1815 @@ class TestTriggersDecoding(unittest.TestCase): "1808B", # fmt: off *[ - 0x17, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0xda, 0xaf, 0x8d, 0x09, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0xda, 0xaf, 0x8d, 0x09, 0x00, 0x00, 0x00, - 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, - 0x38, 0x21, 0x41, 0x42, 0x48, 0x48, 0x48, 0x48, - 0xa0, 0x12, 0xa0, 0xa4, 0x48, 0x48, 0x48, 0x48, - 0x0e, 0x00, 0x00, 0x00, 0x48, 0x48, 0x48, 0x48, - 0x41, 0x00, 0x75, 0x00, 0x74, 0x00, 0x68, 0x00, - 0x6f, 0x00, 0x72, 0x00, 0x00, 0x00, 0x48, 0x48, - 0x00, 0x00, 0x00, 0x00, 0x48, 0x48, 0x48, 0x48, - 0x00, 0x48, 0x48, 0x48, 0x48, 0x48, 0x48, 0x48, - 0x00, 0x48, 0x48, 0x48, 0x48, 0x48, 0x48, 0x48, - 0x01, 0x00, 0x00, 0x00, 0x48, 0x48, 0x48, 0x48, - 0x1c, 0x00, 0x00, 0x00, 0x48, 0x48, 0x48, 0x48, - 0x01, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00, 0x05, - 0x15, 0x00, 0x00, 0x00, 0x69, 0xce, 0x28, 0x2a, - 0xce, 0xd8, 0x1f, 0x77, 0x37, 0x9c, 0xe2, 0x44, - 0xf4, 0x01, 0x00, 0x00, 0x48, 0x48, 0x48, 0x48, - 0x40, 0x00, 0x00, 0x00, 0x48, 0x48, 0x48, 0x48, - 0x44, 0x00, 0x45, 0x00, 0x53, 0x00, 0x4b, 0x00, - 0x54, 0x00, 0x4f, 0x00, 0x50, 0x00, 0x2d, 0x00, - 0x45, 0x00, 0x33, 0x00, 0x38, 0x00, 0x38, 0x00, - 0x44, 0x00, 0x38, 0x00, 0x50, 0x00, 0x5c, 0x00, - 0x41, 0x00, 0x64, 0x00, 0x6d, 0x00, 0x69, 0x00, - 0x6e, 0x00, 0x69, 0x00, 0x73, 0x00, 0x74, 0x00, - 0x72, 0x00, 0x61, 0x00, 0x74, 0x00, 0x6f, 0x00, - 0x72, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x2c, 0x00, 0x00, 0x00, 0x48, 0x48, 0x48, 0x48, - 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, - 0x80, 0xf4, 0x03, 0x00, 0xff, 0xff, 0xff, 0xff, - 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x48, 0x48, 0x48, 0x48, - 0xdd, 0xdd, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x01, 0x07, 0x0a, 0x00, 0x00, 0x00, 0x09, 0x00, - 0x80, 0x48, 0x11, 0xf8, 0x36, 0x1a, 0xdb, 0x01, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x01, 0x2e, 0xe2, 0x01, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0xc2, 0x31, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x48, 0x48, 0x48, 0x48, - 0xaa, 0xaa, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0xda, 0xaf, 0x8d, 0x09, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0xda, 0xaf, 0x8d, 0x09, 0x00, 0x00, 0x00, - 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, - 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, - 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x48, 0x48, 0x48, 0x48, - 0x01, 0x48, 0x48, 0x48, 0x48, 0x48, 0x48, 0x48, - 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0xda, 0xaf, 0x8d, 0x09, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0xda, 0xaf, 0x8d, 0x09, 0x00, 0x00, 0x00, - 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, - 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x48, 0x48, 0x48, 0x48, - 0xee, 0xee, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0xda, 0xaf, 0x8d, 0x09, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0xda, 0xaf, 0x8d, 0x09, 0x00, 0x00, 0x00, - 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, - 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x48, 0x48, 0x48, 0x48, - 0xcc, 0xcc, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, - 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x01, 0x00, 0x65, 0x00, 0x78, 0x00, 0x65, 0x00, - 0x22, 0x00, 0x20, 0x00, 0x53, 0x00, 0x74, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x48, 0x48, 0x48, 0x48, - 0x84, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x3c, 0x00, 0x51, 0x00, 0x75, 0x00, 0x65, 0x00, - 0x72, 0x00, 0x79, 0x00, 0x4c, 0x00, 0x69, 0x00, - 0x73, 0x00, 0x74, 0x00, 0x3e, 0x00, 0x3c, 0x00, - 0x51, 0x00, 0x75, 0x00, 0x65, 0x00, 0x72, 0x00, - 0x79, 0x00, 0x20, 0x00, 0x49, 0x00, 0x64, 0x00, - 0x3d, 0x00, 0x22, 0x00, 0x30, 0x00, 0x22, 0x00, - 0x20, 0x00, 0x50, 0x00, 0x61, 0x00, 0x74, 0x00, - 0x68, 0x00, 0x3d, 0x00, 0x22, 0x00, 0x49, 0x00, - 0x6e, 0x00, 0x74, 0x00, 0x65, 0x00, 0x72, 0x00, - 0x6e, 0x00, 0x65, 0x00, 0x74, 0x00, 0x20, 0x00, - 0x45, 0x00, 0x78, 0x00, 0x70, 0x00, 0x6c, 0x00, - 0x6f, 0x00, 0x72, 0x00, 0x65, 0x00, 0x72, 0x00, - 0x22, 0x00, 0x3e, 0x00, 0x3c, 0x00, 0x53, 0x00, - 0x65, 0x00, 0x6c, 0x00, 0x65, 0x00, 0x63, 0x00, - 0x74, 0x00, 0x20, 0x00, 0x50, 0x00, 0x61, 0x00, - 0x74, 0x00, 0x68, 0x00, 0x3d, 0x00, 0x22, 0x00, - 0x49, 0x00, 0x6e, 0x00, 0x74, 0x00, 0x65, 0x00, - 0x72, 0x00, 0x6e, 0x00, 0x65, 0x00, 0x74, 0x00, - 0x20, 0x00, 0x45, 0x00, 0x78, 0x00, 0x70, 0x00, - 0x6c, 0x00, 0x6f, 0x00, 0x72, 0x00, 0x65, 0x00, - 0x72, 0x00, 0x22, 0x00, 0x3e, 0x00, 0x2a, 0x00, - 0x5b, 0x00, 0x53, 0x00, 0x79, 0x00, 0x73, 0x00, - 0x74, 0x00, 0x65, 0x00, 0x6d, 0x00, 0x5b, 0x00, - 0x45, 0x00, 0x76, 0x00, 0x65, 0x00, 0x6e, 0x00, - 0x74, 0x00, 0x49, 0x00, 0x44, 0x00, 0x3d, 0x00, - 0x32, 0x00, 0x5d, 0x00, 0x5d, 0x00, 0x3c, 0x00, - 0x2f, 0x00, 0x53, 0x00, 0x65, 0x00, 0x6c, 0x00, - 0x65, 0x00, 0x63, 0x00, 0x74, 0x00, 0x3e, 0x00, - 0x3c, 0x00, 0x2f, 0x00, 0x51, 0x00, 0x75, 0x00, - 0x65, 0x00, 0x72, 0x00, 0x79, 0x00, 0x3e, 0x00, - 0x3c, 0x00, 0x2f, 0x00, 0x51, 0x00, 0x75, 0x00, - 0x65, 0x00, 0x72, 0x00, 0x79, 0x00, 0x4c, 0x00, - 0x69, 0x00, 0x73, 0x00, 0x74, 0x00, 0x3e, 0x00, - 0x00, 0x00, 0x48, 0x48, 0x48, 0x48, 0x48, 0x48, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x88, 0x88, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, - 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x48, 0x48, 0x48, 0x48, - 0x77, 0x77, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, - 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, - 0x01, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x48, 0x48, 0x48, 0x48, - 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x01, 0x48, 0x48, 0x48, 0x48, 0x48, 0x48, 0x48, - 0x77, 0x77, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, - 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, - 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x48, 0x48, 0x48, 0x48, - 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x48, 0x48, 0x48, 0x48, 0x48, 0x48, 0x48, - 0x00, 0x48, 0x48, 0x48, 0x48, 0x48, 0x48, 0x48, - 0x01, 0x00, 0x00, 0x00, 0x48, 0x48, 0x48, 0x48, - 0x1c, 0x00, 0x00, 0x00, 0x48, 0x48, 0x48, 0x48, - 0x01, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00, 0x05, - 0x15, 0x00, 0x00, 0x00, 0x69, 0xce, 0x28, 0x2a, - 0xce, 0xd8, 0x1f, 0x77, 0x37, 0x9c, 0xe2, 0x44, - 0xf4, 0x01, 0x00, 0x00, 0x48, 0x48, 0x48, 0x48, - 0x40, 0x00, 0x00, 0x00, 0x48, 0x48, 0x48, 0x48, - 0x44, 0x00, 0x45, 0x00, 0x53, 0x00, 0x4b, 0x00, - 0x54, 0x00, 0x4f, 0x00, 0x50, 0x00, 0x2d, 0x00, - 0x45, 0x00, 0x33, 0x00, 0x38, 0x00, 0x38, 0x00, - 0x44, 0x00, 0x38, 0x00, 0x50, 0x00, 0x5c, 0x00, - 0x41, 0x00, 0x64, 0x00, 0x6d, 0x00, 0x69, 0x00, - 0x6e, 0x00, 0x69, 0x00, 0x73, 0x00, 0x74, 0x00, - 0x72, 0x00, 0x61, 0x00, 0x74, 0x00, 0x6f, 0x00, - 0x72, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x77, 0x77, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, - 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, - 0x01, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x48, 0x48, 0x48, 0x48, - 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x01, 0x48, 0x48, 0x48, 0x48, 0x48, 0x48, 0x48, - 0x77, 0x77, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, - 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, - 0x01, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x48, 0x48, 0x48, 0x48, - 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x48, 0x48, 0x48, 0x48, 0x48, 0x48, 0x48, - 0x00, 0x48, 0x48, 0x48, 0x48, 0x48, 0x48, 0x48, - 0x01, 0x00, 0x00, 0x00, 0x48, 0x48, 0x48, 0x48, - 0x1c, 0x00, 0x00, 0x00, 0x48, 0x48, 0x48, 0x48, - 0x01, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00, 0x05, - 0x15, 0x00, 0x00, 0x00, 0x69, 0xce, 0x28, 0x2a, - 0xce, 0xd8, 0x1f, 0x77, 0x37, 0x9c, 0xe2, 0x44, - 0xf4, 0x01, 0x00, 0x00, 0x48, 0x48, 0x48, 0x48, - 0x40, 0x00, 0x00, 0x00, 0x48, 0x48, 0x48, 0x48, - 0x44, 0x00, 0x45, 0x00, 0x53, 0x00, 0x4b, 0x00, - 0x54, 0x00, 0x4f, 0x00, 0x50, 0x00, 0x2d, 0x00, - 0x45, 0x00, 0x33, 0x00, 0x38, 0x00, 0x38, 0x00, - 0x44, 0x00, 0x38, 0x00, 0x50, 0x00, 0x5c, 0x00, - 0x41, 0x00, 0x64, 0x00, 0x6d, 0x00, 0x69, 0x00, - 0x6e, 0x00, 0x69, 0x00, 0x73, 0x00, 0x74, 0x00, - 0x72, 0x00, 0x61, 0x00, 0x74, 0x00, 0x6f, 0x00, - 0x72, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 - ] + 0x17, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0xDA, + 0xAF, + 0x8D, + 0x09, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0xDA, + 0xAF, + 0x8D, + 0x09, + 0x00, + 0x00, + 0x00, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0x38, + 0x21, + 0x41, + 0x42, + 0x48, + 0x48, + 0x48, + 0x48, + 0xA0, + 0x12, + 0xA0, + 0xA4, + 0x48, + 0x48, + 0x48, + 0x48, + 0x0E, + 0x00, + 0x00, + 0x00, + 0x48, + 0x48, + 0x48, + 0x48, + 0x41, + 0x00, + 0x75, + 0x00, + 0x74, + 0x00, + 0x68, + 0x00, + 0x6F, + 0x00, + 0x72, + 0x00, + 0x00, + 0x00, + 0x48, + 0x48, + 0x00, + 0x00, + 0x00, + 0x00, + 0x48, + 0x48, + 0x48, + 0x48, + 0x00, + 0x48, + 0x48, + 0x48, + 0x48, + 0x48, + 0x48, + 0x48, + 0x00, + 0x48, + 0x48, + 0x48, + 0x48, + 0x48, + 0x48, + 0x48, + 0x01, + 0x00, + 0x00, + 0x00, + 0x48, + 0x48, + 0x48, + 0x48, + 0x1C, + 0x00, + 0x00, + 0x00, + 0x48, + 0x48, + 0x48, + 0x48, + 0x01, + 0x05, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x05, + 0x15, + 0x00, + 0x00, + 0x00, + 0x69, + 0xCE, + 0x28, + 0x2A, + 0xCE, + 0xD8, + 0x1F, + 0x77, + 0x37, + 0x9C, + 0xE2, + 0x44, + 0xF4, + 0x01, + 0x00, + 0x00, + 0x48, + 0x48, + 0x48, + 0x48, + 0x40, + 0x00, + 0x00, + 0x00, + 0x48, + 0x48, + 0x48, + 0x48, + 0x44, + 0x00, + 0x45, + 0x00, + 0x53, + 0x00, + 0x4B, + 0x00, + 0x54, + 0x00, + 0x4F, + 0x00, + 0x50, + 0x00, + 0x2D, + 0x00, + 0x45, + 0x00, + 0x33, + 0x00, + 0x38, + 0x00, + 0x38, + 0x00, + 0x44, + 0x00, + 0x38, + 0x00, + 0x50, + 0x00, + 0x5C, + 0x00, + 0x41, + 0x00, + 0x64, + 0x00, + 0x6D, + 0x00, + 0x69, + 0x00, + 0x6E, + 0x00, + 0x69, + 0x00, + 0x73, + 0x00, + 0x74, + 0x00, + 0x72, + 0x00, + 0x61, + 0x00, + 0x74, + 0x00, + 0x6F, + 0x00, + 0x72, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x2C, + 0x00, + 0x00, + 0x00, + 0x48, + 0x48, + 0x48, + 0x48, + 0x00, + 0x00, + 0x00, + 0x00, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0x80, + 0xF4, + 0x03, + 0x00, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0x07, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x48, + 0x48, + 0x48, + 0x48, + 0xDD, + 0xDD, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x01, + 0x07, + 0x0A, + 0x00, + 0x00, + 0x00, + 0x09, + 0x00, + 0x80, + 0x48, + 0x11, + 0xF8, + 0x36, + 0x1A, + 0xDB, + 0x01, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x01, + 0x2E, + 0xE2, + 0x01, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0xC2, + 0x31, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x48, + 0x48, + 0x48, + 0x48, + 0xAA, + 0xAA, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0xDA, + 0xAF, + 0x8D, + 0x09, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0xDA, + 0xAF, + 0x8D, + 0x09, + 0x00, + 0x00, + 0x00, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0x00, + 0x00, + 0x00, + 0x00, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x01, + 0x00, + 0x00, + 0x01, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x48, + 0x48, + 0x48, + 0x48, + 0x01, + 0x48, + 0x48, + 0x48, + 0x48, + 0x48, + 0x48, + 0x48, + 0xFF, + 0xFF, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0xDA, + 0xAF, + 0x8D, + 0x09, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0xDA, + 0xAF, + 0x8D, + 0x09, + 0x00, + 0x00, + 0x00, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0x00, + 0x00, + 0x00, + 0x00, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x01, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x48, + 0x48, + 0x48, + 0x48, + 0xEE, + 0xEE, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0xDA, + 0xAF, + 0x8D, + 0x09, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0xDA, + 0xAF, + 0x8D, + 0x09, + 0x00, + 0x00, + 0x00, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0x00, + 0x00, + 0x00, + 0x00, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x01, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x48, + 0x48, + 0x48, + 0x48, + 0xCC, + 0xCC, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0x00, + 0x00, + 0x00, + 0x00, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x01, + 0x00, + 0x65, + 0x00, + 0x78, + 0x00, + 0x65, + 0x00, + 0x22, + 0x00, + 0x20, + 0x00, + 0x53, + 0x00, + 0x74, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x48, + 0x48, + 0x48, + 0x48, + 0x84, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x3C, + 0x00, + 0x51, + 0x00, + 0x75, + 0x00, + 0x65, + 0x00, + 0x72, + 0x00, + 0x79, + 0x00, + 0x4C, + 0x00, + 0x69, + 0x00, + 0x73, + 0x00, + 0x74, + 0x00, + 0x3E, + 0x00, + 0x3C, + 0x00, + 0x51, + 0x00, + 0x75, + 0x00, + 0x65, + 0x00, + 0x72, + 0x00, + 0x79, + 0x00, + 0x20, + 0x00, + 0x49, + 0x00, + 0x64, + 0x00, + 0x3D, + 0x00, + 0x22, + 0x00, + 0x30, + 0x00, + 0x22, + 0x00, + 0x20, + 0x00, + 0x50, + 0x00, + 0x61, + 0x00, + 0x74, + 0x00, + 0x68, + 0x00, + 0x3D, + 0x00, + 0x22, + 0x00, + 0x49, + 0x00, + 0x6E, + 0x00, + 0x74, + 0x00, + 0x65, + 0x00, + 0x72, + 0x00, + 0x6E, + 0x00, + 0x65, + 0x00, + 0x74, + 0x00, + 0x20, + 0x00, + 0x45, + 0x00, + 0x78, + 0x00, + 0x70, + 0x00, + 0x6C, + 0x00, + 0x6F, + 0x00, + 0x72, + 0x00, + 0x65, + 0x00, + 0x72, + 0x00, + 0x22, + 0x00, + 0x3E, + 0x00, + 0x3C, + 0x00, + 0x53, + 0x00, + 0x65, + 0x00, + 0x6C, + 0x00, + 0x65, + 0x00, + 0x63, + 0x00, + 0x74, + 0x00, + 0x20, + 0x00, + 0x50, + 0x00, + 0x61, + 0x00, + 0x74, + 0x00, + 0x68, + 0x00, + 0x3D, + 0x00, + 0x22, + 0x00, + 0x49, + 0x00, + 0x6E, + 0x00, + 0x74, + 0x00, + 0x65, + 0x00, + 0x72, + 0x00, + 0x6E, + 0x00, + 0x65, + 0x00, + 0x74, + 0x00, + 0x20, + 0x00, + 0x45, + 0x00, + 0x78, + 0x00, + 0x70, + 0x00, + 0x6C, + 0x00, + 0x6F, + 0x00, + 0x72, + 0x00, + 0x65, + 0x00, + 0x72, + 0x00, + 0x22, + 0x00, + 0x3E, + 0x00, + 0x2A, + 0x00, + 0x5B, + 0x00, + 0x53, + 0x00, + 0x79, + 0x00, + 0x73, + 0x00, + 0x74, + 0x00, + 0x65, + 0x00, + 0x6D, + 0x00, + 0x5B, + 0x00, + 0x45, + 0x00, + 0x76, + 0x00, + 0x65, + 0x00, + 0x6E, + 0x00, + 0x74, + 0x00, + 0x49, + 0x00, + 0x44, + 0x00, + 0x3D, + 0x00, + 0x32, + 0x00, + 0x5D, + 0x00, + 0x5D, + 0x00, + 0x3C, + 0x00, + 0x2F, + 0x00, + 0x53, + 0x00, + 0x65, + 0x00, + 0x6C, + 0x00, + 0x65, + 0x00, + 0x63, + 0x00, + 0x74, + 0x00, + 0x3E, + 0x00, + 0x3C, + 0x00, + 0x2F, + 0x00, + 0x51, + 0x00, + 0x75, + 0x00, + 0x65, + 0x00, + 0x72, + 0x00, + 0x79, + 0x00, + 0x3E, + 0x00, + 0x3C, + 0x00, + 0x2F, + 0x00, + 0x51, + 0x00, + 0x75, + 0x00, + 0x65, + 0x00, + 0x72, + 0x00, + 0x79, + 0x00, + 0x4C, + 0x00, + 0x69, + 0x00, + 0x73, + 0x00, + 0x74, + 0x00, + 0x3E, + 0x00, + 0x00, + 0x00, + 0x48, + 0x48, + 0x48, + 0x48, + 0x48, + 0x48, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x88, + 0x88, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0x00, + 0x00, + 0x00, + 0x00, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x01, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x48, + 0x48, + 0x48, + 0x48, + 0x77, + 0x77, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0x00, + 0x00, + 0x00, + 0x00, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x01, + 0x00, + 0x00, + 0x01, + 0xFF, + 0xFF, + 0xFF, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x48, + 0x48, + 0x48, + 0x48, + 0x03, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x01, + 0x48, + 0x48, + 0x48, + 0x48, + 0x48, + 0x48, + 0x48, + 0x77, + 0x77, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0x00, + 0x00, + 0x00, + 0x00, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x01, + 0x00, + 0x00, + 0x01, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x48, + 0x48, + 0x48, + 0x48, + 0x04, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x48, + 0x48, + 0x48, + 0x48, + 0x48, + 0x48, + 0x48, + 0x00, + 0x48, + 0x48, + 0x48, + 0x48, + 0x48, + 0x48, + 0x48, + 0x01, + 0x00, + 0x00, + 0x00, + 0x48, + 0x48, + 0x48, + 0x48, + 0x1C, + 0x00, + 0x00, + 0x00, + 0x48, + 0x48, + 0x48, + 0x48, + 0x01, + 0x05, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x05, + 0x15, + 0x00, + 0x00, + 0x00, + 0x69, + 0xCE, + 0x28, + 0x2A, + 0xCE, + 0xD8, + 0x1F, + 0x77, + 0x37, + 0x9C, + 0xE2, + 0x44, + 0xF4, + 0x01, + 0x00, + 0x00, + 0x48, + 0x48, + 0x48, + 0x48, + 0x40, + 0x00, + 0x00, + 0x00, + 0x48, + 0x48, + 0x48, + 0x48, + 0x44, + 0x00, + 0x45, + 0x00, + 0x53, + 0x00, + 0x4B, + 0x00, + 0x54, + 0x00, + 0x4F, + 0x00, + 0x50, + 0x00, + 0x2D, + 0x00, + 0x45, + 0x00, + 0x33, + 0x00, + 0x38, + 0x00, + 0x38, + 0x00, + 0x44, + 0x00, + 0x38, + 0x00, + 0x50, + 0x00, + 0x5C, + 0x00, + 0x41, + 0x00, + 0x64, + 0x00, + 0x6D, + 0x00, + 0x69, + 0x00, + 0x6E, + 0x00, + 0x69, + 0x00, + 0x73, + 0x00, + 0x74, + 0x00, + 0x72, + 0x00, + 0x61, + 0x00, + 0x74, + 0x00, + 0x6F, + 0x00, + 0x72, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x77, + 0x77, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0x00, + 0x00, + 0x00, + 0x00, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x01, + 0x00, + 0x00, + 0x01, + 0xFF, + 0xFF, + 0xFF, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x48, + 0x48, + 0x48, + 0x48, + 0x07, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x01, + 0x48, + 0x48, + 0x48, + 0x48, + 0x48, + 0x48, + 0x48, + 0x77, + 0x77, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0x00, + 0x00, + 0x00, + 0x00, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x01, + 0x00, + 0x00, + 0x01, + 0xFF, + 0xFF, + 0xFF, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x48, + 0x48, + 0x48, + 0x48, + 0x08, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x48, + 0x48, + 0x48, + 0x48, + 0x48, + 0x48, + 0x48, + 0x00, + 0x48, + 0x48, + 0x48, + 0x48, + 0x48, + 0x48, + 0x48, + 0x01, + 0x00, + 0x00, + 0x00, + 0x48, + 0x48, + 0x48, + 0x48, + 0x1C, + 0x00, + 0x00, + 0x00, + 0x48, + 0x48, + 0x48, + 0x48, + 0x01, + 0x05, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x05, + 0x15, + 0x00, + 0x00, + 0x00, + 0x69, + 0xCE, + 0x28, + 0x2A, + 0xCE, + 0xD8, + 0x1F, + 0x77, + 0x37, + 0x9C, + 0xE2, + 0x44, + 0xF4, + 0x01, + 0x00, + 0x00, + 0x48, + 0x48, + 0x48, + 0x48, + 0x40, + 0x00, + 0x00, + 0x00, + 0x48, + 0x48, + 0x48, + 0x48, + 0x44, + 0x00, + 0x45, + 0x00, + 0x53, + 0x00, + 0x4B, + 0x00, + 0x54, + 0x00, + 0x4F, + 0x00, + 0x50, + 0x00, + 0x2D, + 0x00, + 0x45, + 0x00, + 0x33, + 0x00, + 0x38, + 0x00, + 0x38, + 0x00, + 0x44, + 0x00, + 0x38, + 0x00, + 0x50, + 0x00, + 0x5C, + 0x00, + 0x41, + 0x00, + 0x64, + 0x00, + 0x6D, + 0x00, + 0x69, + 0x00, + 0x6E, + 0x00, + 0x69, + 0x00, + 0x73, + 0x00, + 0x74, + 0x00, + 0x72, + 0x00, + 0x61, + 0x00, + 0x74, + 0x00, + 0x6F, + 0x00, + 0x72, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + ], # fmt: on ) triggers = scheduled_tasks.TriggerSet.decode(buf) diff --git a/test/renderers/test_parquet_renderers.py b/test/renderers/test_parquet_renderers.py index aa5474636..c9c07b2db 100644 --- a/test/renderers/test_parquet_renderers.py +++ b/test/renderers/test_parquet_renderers.py @@ -8,9 +8,10 @@ try: import pyarrow as pa import pyarrow.parquet as pq import pyarrow.compute as pc + HAS_PYARROW = True except ImportError: - # The user doesn't have pyarrow installed, but HAS_PYARROW will be false so just continue + # The user doesn't have pyarrow installed, but HAS_PYARROW will be false so just continue pass @@ -41,10 +42,33 @@ class TestArrowRendererBase(ABC): table = self._get_table_from_output(out) assert table.num_rows > 10 - assert table.filter(pc.match_substring(pc.utf8_lower(table.column('ImageFileName')), "system")).num_rows > 0 - assert table.filter(pc.match_substring(pc.utf8_lower(table.column('ImageFileName')), "csrss.exe")).num_rows > 0 - assert table.filter(pc.match_substring(pc.utf8_lower(table.column('ImageFileName')), "svchost.exe")).num_rows > 0 - assert table.filter(pc.greater(table.column('PID'), 0)).num_rows == table.num_rows + assert ( + table.filter( + pc.match_substring( + pc.utf8_lower(table.column("ImageFileName")), "system" + ) + ).num_rows + > 0 + ) + assert ( + table.filter( + pc.match_substring( + pc.utf8_lower(table.column("ImageFileName")), "csrss.exe" + ) + ).num_rows + > 0 + ) + assert ( + table.filter( + pc.match_substring( + pc.utf8_lower(table.column("ImageFileName")), "svchost.exe" + ) + ).num_rows + > 0 + ) + assert ( + table.filter(pc.greater(table.column("PID"), 0)).num_rows == table.num_rows + ) def test_linux_generic_pslist(self, volatility, python, image): rc, out, _err = test_volatility.runvol_plugin( @@ -59,12 +83,23 @@ class TestArrowRendererBase(ABC): table = self._get_table_from_output(out) assert table.num_rows > 10 - init_rows = table.filter(pc.match_substring(pc.utf8_lower(table.column('COMM')), "init")) - systemd_rows = table.filter(pc.match_substring(pc.utf8_lower(table.column('COMM')), "systemd")) + init_rows = table.filter( + pc.match_substring(pc.utf8_lower(table.column("COMM")), "init") + ) + systemd_rows = table.filter( + pc.match_substring(pc.utf8_lower(table.column("COMM")), "systemd") + ) assert (init_rows.num_rows > 0) or (systemd_rows.num_rows > 0) - assert table.filter(pc.match_substring(pc.utf8_lower(table.column('COMM')), "watchdog")).num_rows > 0 - assert table.filter(pc.greater(table.column('PID'), 0)).num_rows == table.num_rows + assert ( + table.filter( + pc.match_substring(pc.utf8_lower(table.column("COMM")), "watchdog") + ).num_rows + > 0 + ) + assert ( + table.filter(pc.greater(table.column("PID"), 0)).num_rows == table.num_rows + ) def test_windows_generic_handles(self, volatility, python, image): rc, out, _err = test_volatility.runvol_plugin( @@ -79,7 +114,14 @@ class TestArrowRendererBase(ABC): table = self._get_table_from_output(out) assert table.num_rows > 500 - assert table.filter(pc.match_substring(pc.utf8_lower(table.column('Name')), "machine\\system")).num_rows > 0 + assert ( + table.filter( + pc.match_substring( + pc.utf8_lower(table.column("Name")), "machine\\system" + ) + ).num_rows + > 0 + ) def test_linux_generic_lsof(self, volatility, python, image): rc, out, _err = test_volatility.runvol_plugin( @@ -94,6 +136,7 @@ class TestArrowRendererBase(ABC): table = self._get_table_from_output(out) assert table.num_rows > 35 + class TestParquetRenderer(TestArrowRendererBase): renderer_format = "parquet" @@ -106,4 +149,3 @@ class TestArrowRenderer(TestArrowRendererBase): def _get_table_from_output(self, output_bytes): return pa.ipc.open_stream(io.BytesIO(output_bytes)).read_all() - diff --git a/test/volatility3_code_analysis.py b/test/volatility3_code_analysis.py index 100ad3074..43fed459b 100644 --- a/test/volatility3_code_analysis.py +++ b/test/volatility3_code_analysis.py @@ -82,7 +82,6 @@ class CodeViolation(metaclass=abc.ABCMeta): class UnrequiredVersionableUsage(CodeViolation): - def __init__( self, module: types.ModuleType, @@ -107,7 +106,6 @@ class UnrequiredVersionableUsage(CodeViolation): class DirectVolatilityImportUsage(CodeViolation): - def __init__( self, module: types.ModuleType, @@ -174,8 +172,11 @@ class ModuleVisitor(NodeVisitor): """ if ( node.module - and node.module.startswith("volatility3.") # Give a pass to volatility3 module - and node.module != "volatility3.framework.constants._version" # make an exception for this + 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: try: @@ -204,7 +205,6 @@ 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 diff --git a/volatility3/__init__.py b/volatility3/__init__.py index 94a6721e1..9867f5d94 100644 --- a/volatility3/__init__.py +++ b/volatility3/__init__.py @@ -2,6 +2,7 @@ # which is available at https://www.volatilityfoundation.org/license/vsl-v1.0 # """Volatility 3 - An open-source memory forensics framework""" + import inspect import sys from importlib import abc diff --git a/volatility3/cli/__init__.py b/volatility3/cli/__init__.py index 4c379a15e..15e6cc7b4 100644 --- a/volatility3/cli/__init__.py +++ b/volatility3/cli/__init__.py @@ -10,6 +10,7 @@ User interfaces make use of the framework to: * run the plugin * display the results """ + import argparse import inspect import io @@ -458,8 +459,9 @@ class CommandLine: raise ValueError( "Invalid extension (extensions must be of the format \"conf.path.value='value'\")" ) - address, value = extension[: extension.find("=")], json.loads( - extension[extension.find("=") + 1 :] + address, value = ( + extension[: extension.find("=")], + json.loads(extension[extension.find("=") + 1 :]), ) ctx.config[address] = value @@ -574,7 +576,7 @@ class CommandLine: delayed_logs.append( ( logging.DEBUG, - f"Loaded configuration: {json.dumps(result, indent = 2, sort_keys = True)}", + f"Loaded configuration: {json.dumps(result, indent=2, sort_keys=True)}", ) ) return delayed_logs, result @@ -763,7 +765,7 @@ class CommandLine: constants.LOGLEVEL_VVVV, ] ): - logging.addLevelName(level_value, f"DETAIL {level+1}") + logging.addLevelName(level_value, f"DETAIL {level + 1}") def file_handler_class_factory(self, direct=True): output_dir = self.output_dir diff --git a/volatility3/cli/text_renderer.py b/volatility3/cli/text_renderer.py index 1e437a0be..d55201371 100644 --- a/volatility3/cli/text_renderer.py +++ b/volatility3/cli/text_renderer.py @@ -278,7 +278,6 @@ class CLIRenderer(interfaces.renderers.Renderer): class QuickTextRenderer(CLIRenderer): - name = "quick" def get_render_options(self): @@ -348,7 +347,6 @@ class NoneRenderer(CLIRenderer): class CSVRenderer(CLIRenderer): - name = "csv" structured_output = True diff --git a/volatility3/cli/volshell/__init__.py b/volatility3/cli/volshell/__init__.py index 0affe5d59..5bc29f220 100644 --- a/volatility3/cli/volshell/__init__.py +++ b/volatility3/cli/volshell/__init__.py @@ -344,8 +344,9 @@ class VolShell(cli.CommandLine): raise ValueError( "Invalid extension (extensions must be of the format \"conf.path.value='value'\")" ) - address, value = extension[: extension.find("=")], json.loads( - extension[extension.find("=") + 1 :] + address, value = ( + extension[: extension.find("=")], + json.loads(extension[extension.find("=") + 1 :]), ) ctx.config[address] = value diff --git a/volatility3/cli/volshell/generic.py b/volatility3/cli/volshell/generic.py index ace4b2119..2a8039ff2 100644 --- a/volatility3/cli/volshell/generic.py +++ b/volatility3/cli/volshell/generic.py @@ -469,7 +469,7 @@ class Volshell(interfaces.plugins.PluginInterface): and dereference_count < MAX_DEREFERENCE_COUNT ): # before defreerencing the pointer, show it's information - print(f'{" " * dereference_count}{self._display_simple_type(volobject)}') + print(f"{' ' * dereference_count}{self._display_simple_type(volobject)}") # check that we can follow the pointer before dereferencing and do not # attempt to follow null pointers. @@ -486,7 +486,7 @@ class Volshell(interfaces.plugins.PluginInterface): if hasattr(volobject.vol, "members"): # display the header for this object, if the original object was just a type string, display the type information - struct_header = f'{" " * dereference_count}{volobject.vol.type_name} ({volobject.vol.size} bytes)' + struct_header = f"{' ' * dereference_count}{volobject.vol.type_name} ({volobject.vol.size} bytes)" if isinstance(object, str) and offset is None: suffix = ":" else: @@ -523,7 +523,7 @@ class Volshell(interfaces.plugins.PluginInterface): len_typename = len(member_type_name) if len(member_type_name) > MAX_TYPENAME_DISPLAY_LENGTH: len_typename = MAX_TYPENAME_DISPLAY_LENGTH - member_type_name = f"{member_type_name[:len_typename - 3]}..." + member_type_name = f"{member_type_name[: len_typename - 3]}..." if isinstance(volobject, interfaces.objects.ObjectInterface): # We're an instance, so also display the data diff --git a/volatility3/framework/__init__.py b/volatility3/framework/__init__.py index 1c899f434..6943598cf 100644 --- a/volatility3/framework/__init__.py +++ b/volatility3/framework/__init__.py @@ -2,6 +2,7 @@ # which is available at https://www.volatilityfoundation.org/license/vsl-v1.0 # """Volatility 3 framework.""" + # Check the python version to ensure it's suitable import glob import sys diff --git a/volatility3/framework/automagic/pdbscan.py b/volatility3/framework/automagic/pdbscan.py index 55b9b81e1..8e2233840 100644 --- a/volatility3/framework/automagic/pdbscan.py +++ b/volatility3/framework/automagic/pdbscan.py @@ -7,6 +7,7 @@ from loaded PE files. This module contains a standalone scanner, and also a :class:`~volatility3.framework.interfaces.layers.ScannerInterface` based scanner for use within the framework by calling :func:`~volatility3.framework.interfaces.layers.DataLayerInterface.scan`. """ + import contextlib import logging import math diff --git a/volatility3/framework/automagic/stacker.py b/volatility3/framework/automagic/stacker.py index 596864264..2e9875086 100644 --- a/volatility3/framework/automagic/stacker.py +++ b/volatility3/framework/automagic/stacker.py @@ -153,8 +153,9 @@ class LayerStacker(interfaces.automagic.AutomagicInterface): constructor(context, config_path, requirement) # Stash the changed config items - self._cached = context.config.get(path, None), context.config.branch( - path + self._cached = ( + context.config.get(path, None), + context.config.branch(path), ) vollog.debug( f"physical_layer maximum_address: {physical_layer.maximum_address}" diff --git a/volatility3/framework/automagic/windows.py b/volatility3/framework/automagic/windows.py index 52296f5ad..7d56b01b3 100644 --- a/volatility3/framework/automagic/windows.py +++ b/volatility3/framework/automagic/windows.py @@ -26,6 +26,7 @@ The self-referential indices for older versions of windows are listed below: | x64 | 0x1ED | +--------------+-------+ """ + import logging import struct from typing import Generator, Iterable, List, Optional, Tuple, Type diff --git a/volatility3/framework/configuration/requirements.py b/volatility3/framework/configuration/requirements.py index 6d978e2a9..b1cc716e5 100644 --- a/volatility3/framework/configuration/requirements.py +++ b/volatility3/framework/configuration/requirements.py @@ -8,6 +8,7 @@ These requirement types allow plugins to request simple information types (such as strings, integers, etc) as well as indicating what they expect to be in the context (such as particular layers or symboltables). """ + import abc import logging import os diff --git a/volatility3/framework/constants/linux/__init__.py b/volatility3/framework/constants/linux/__init__.py index f45bed926..3f8c52b43 100644 --- a/volatility3/framework/constants/linux/__init__.py +++ b/volatility3/framework/constants/linux/__init__.py @@ -5,6 +5,7 @@ Linux-specific values that aren't found in debug symbols """ + import enum from dataclasses import dataclass diff --git a/volatility3/framework/contexts/__init__.py b/volatility3/framework/contexts/__init__.py index 32d33f657..125f41f8a 100644 --- a/volatility3/framework/contexts/__init__.py +++ b/volatility3/framework/contexts/__init__.py @@ -8,6 +8,7 @@ This has been made an object to allow quick swapping and changing of contexts, to allow a plugin to act on multiple different contexts without them interfering with each other. """ + import functools import hashlib import logging diff --git a/volatility3/framework/deprecation.py b/volatility3/framework/deprecation.py index 90fa9bce0..581647922 100644 --- a/volatility3/framework/deprecation.py +++ b/volatility3/framework/deprecation.py @@ -79,7 +79,7 @@ def deprecated_method( "This is a bug, the deprecated call needs to be removed and the caller needs to update their code to use the new method.", ) - deprecation_msg = f"Method \"{deprecated_func.__module__ + '.' + deprecated_func.__qualname__}\" is deprecated and will be removed in the first release after {removal_date}, use \"{replacement.__module__ + '.' + replacement.__qualname__}\" instead. {additional_information}" + deprecation_msg = f'Method "{deprecated_func.__module__ + "." + deprecated_func.__qualname__}" is deprecated and will be removed in the first release after {removal_date}, use "{replacement.__module__ + "." + replacement.__qualname__}" instead. {additional_information}' warnings.warn(deprecation_msg, FutureWarning) # Return the wrapped function with its original arguments return deprecated_func(*args, **kwargs) diff --git a/volatility3/framework/exceptions.py b/volatility3/framework/exceptions.py index 34b41727a..3b70c5c29 100644 --- a/volatility3/framework/exceptions.py +++ b/volatility3/framework/exceptions.py @@ -8,6 +8,7 @@ space or symbol tables, and by layers when an address is invalid. The :class:`PagedInvalidAddressException` contains information about the size of the invalid page. """ + from typing import Callable, Dict, Optional, Tuple from volatility3.framework import interfaces @@ -161,4 +162,4 @@ class VersionMismatchException(VolatilityException): self.failure_reason = failure_reason def __str__(self): - return f"{self.source_component.__module__+ '.' + self.source_component.__qualname__}: Version {self.target_version} dependency on {self.target_component.__module__+ '.' + self.target_component.__name__} {self.target_component.version} unmet." + return f"{self.source_component.__module__ + '.' + self.source_component.__qualname__}: Version {self.target_version} dependency on {self.target_component.__module__ + '.' + self.target_component.__name__} {self.target_component.version} unmet." diff --git a/volatility3/framework/interfaces/automagic.py b/volatility3/framework/interfaces/automagic.py index 4ac386fc0..744a33bab 100644 --- a/volatility3/framework/interfaces/automagic.py +++ b/volatility3/framework/interfaces/automagic.py @@ -7,6 +7,7 @@ runs. Automagic objects attempt to automatically fill configuration values that a user has not filled. """ + import logging from abc import ABCMeta from typing import Any, List, Optional, Tuple, Type, Union diff --git a/volatility3/framework/interfaces/context.py b/volatility3/framework/interfaces/context.py index 4f863898e..0b71e4cb2 100644 --- a/volatility3/framework/interfaces/context.py +++ b/volatility3/framework/interfaces/context.py @@ -11,6 +11,7 @@ convenience functions, most notably the object constructor function, `object`, which will construct a symbol on a layer at a particular offset. """ + import collections import copy from abc import ABCMeta, abstractmethod diff --git a/volatility3/framework/interfaces/layers.py b/volatility3/framework/interfaces/layers.py index 6c4e4b419..2e328124d 100644 --- a/volatility3/framework/interfaces/layers.py +++ b/volatility3/framework/interfaces/layers.py @@ -6,6 +6,7 @@ One layer may combine other layers, map data based on the data itself, or map a procedure (such as decryption) across another layer of data. """ + import collections.abc import functools import logging diff --git a/volatility3/framework/interfaces/objects.py b/volatility3/framework/interfaces/objects.py index 2d8024465..7f67cf586 100644 --- a/volatility3/framework/interfaces/objects.py +++ b/volatility3/framework/interfaces/objects.py @@ -3,6 +3,7 @@ # """Objects are the core of volatility, and provide pythonic access to interpreted values of data from a layer.""" + import abc import collections import collections.abc diff --git a/volatility3/framework/interfaces/symbols.py b/volatility3/framework/interfaces/symbols.py index 925be72c9..1159fd290 100644 --- a/volatility3/framework/interfaces/symbols.py +++ b/volatility3/framework/interfaces/symbols.py @@ -2,6 +2,7 @@ # which is available at https://www.volatilityfoundation.org/license/vsl-v1.0 # """Symbols provide structural information about a set of bytes.""" + import bisect import collections.abc from abc import ABC, abstractmethod diff --git a/volatility3/framework/layers/avml.py b/volatility3/framework/layers/avml.py index 2e5572192..7c052f70a 100644 --- a/volatility3/framework/layers/avml.py +++ b/volatility3/framework/layers/avml.py @@ -6,6 +6,7 @@ The user of the file doesn't have to worry about the compression, but random access is not allowed.""" + import ctypes import logging import struct diff --git a/volatility3/framework/layers/codecs/__init__.py b/volatility3/framework/layers/codecs/__init__.py index e019bcbcd..5b9f2602b 100644 --- a/volatility3/framework/layers/codecs/__init__.py +++ b/volatility3/framework/layers/codecs/__init__.py @@ -2,7 +2,4 @@ # which is available at https://www.volatilityfoundation.org/license/vsl-v1.0 # -"""Codecs used for encoding or decoding data should live here - - -""" +"""Codecs used for encoding or decoding data should live here""" diff --git a/volatility3/framework/layers/intel.py b/volatility3/framework/layers/intel.py index 696c33353..848580004 100644 --- a/volatility3/framework/layers/intel.py +++ b/volatility3/framework/layers/intel.py @@ -315,7 +315,13 @@ class Intel(linear.LinearlyMappedLayer): ): # The block isn't contiguous if stashed_offset is not None: - yield stashed_offset, stashed_size, stashed_mapped_offset, stashed_mapped_size, stashed_map_layer + yield ( + stashed_offset, + stashed_size, + stashed_mapped_offset, + stashed_mapped_size, + stashed_map_layer, + ) # Update all the stashed values after output stashed_offset = offset stashed_mapped_offset = mapped_offset @@ -334,7 +340,13 @@ class Intel(linear.LinearlyMappedLayer): and stashed_mapped_size is not None and stashed_map_layer is not None ): - yield stashed_offset, stashed_size, stashed_mapped_offset, stashed_mapped_size, stashed_map_layer + yield ( + stashed_offset, + stashed_size, + stashed_mapped_offset, + stashed_mapped_size, + stashed_map_layer, + ) def _mapping( self, offset: int, length: int, ignore_errors: bool = False diff --git a/volatility3/framework/layers/msf.py b/volatility3/framework/layers/msf.py index 2b4fae963..a7bf6466e 100644 --- a/volatility3/framework/layers/msf.py +++ b/volatility3/framework/layers/msf.py @@ -234,9 +234,13 @@ class PdbMSFStream(linear.LinearlyMappedLayer): layer_name=self.name, invalid_address=offset + returned ) else: - yield offset + returned, chunk_size, ( - self._pages[page] * page_size - ) + page_position, chunk_size, self._base_layer + yield ( + offset + returned, + chunk_size, + (self._pages[page] * page_size) + page_position, + chunk_size, + self._base_layer, + ) returned += chunk_size length -= chunk_size diff --git a/volatility3/framework/layers/resources.py b/volatility3/framework/layers/resources.py index 6121c2cff..66fb617af 100644 --- a/volatility3/framework/layers/resources.py +++ b/volatility3/framework/layers/resources.py @@ -305,8 +305,9 @@ class JarHandler(VolatilityHandler): def default_open(req: urllib.request.Request) -> Optional[Any]: """Handles the request if it's the jar scheme.""" if req.type == "jar": - subscheme, remainder = req.full_url.split(":")[1], ":".join( - req.full_url.split(":")[2:] + subscheme, remainder = ( + req.full_url.split(":")[1], + ":".join(req.full_url.split(":")[2:]), ) if subscheme != "file": vollog.log( diff --git a/volatility3/framework/layers/segmented.py b/volatility3/framework/layers/segmented.py index 9825ae15c..96b9618dc 100644 --- a/volatility3/framework/layers/segmented.py +++ b/volatility3/framework/layers/segmented.py @@ -129,7 +129,13 @@ class NonLinearlySegmentedLayer( return None # Crop it to the amount we need left chunk_size = min(size, length + offset - logical_offset) - yield logical_offset, chunk_size, mapped_offset, mapped_size, self._base_layer + yield ( + logical_offset, + chunk_size, + mapped_offset, + mapped_size, + self._base_layer, + ) current_offset += chunk_size # Terminate if we've gone (or reached) our required limit if current_offset >= offset + length: diff --git a/volatility3/framework/layers/vmware.py b/volatility3/framework/layers/vmware.py index 39fb21b63..5dc00f344 100644 --- a/volatility3/framework/layers/vmware.py +++ b/volatility3/framework/layers/vmware.py @@ -65,10 +65,10 @@ class VmwareLayer(segmented.SegmentedLayer): data = meta_layer.read(0, header_size) magic, unknown, groupCount = struct.unpack(self.header_structure, data) if magic not in [ - b"\xD0\xBE\xD2\xBE", - b"\xD1\xBA\xD1\xBA", - b"\xD2\xBE\xD2\xBE", - b"\xD3\xBE\xD3\xBE", + b"\xd0\xbe\xd2\xbe", + b"\xd1\xba\xd1\xba", + b"\xd2\xbe\xd2\xbe", + b"\xd3\xbe\xd3\xbe", ]: raise VmwareFormatException( self.name, f"Wrong magic bytes for Vmware layer: {repr(magic)}" diff --git a/volatility3/framework/plugins/banners.py b/volatility3/framework/plugins/banners.py index d4e6e2aa8..eea39206d 100644 --- a/volatility3/framework/plugins/banners.py +++ b/volatility3/framework/plugins/banners.py @@ -60,8 +60,9 @@ class Banners(interfaces.plugins.PluginInterface): not in b" #()+,;/-.0123456789:@ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz~" ] if not failed: - yield format_hints.Hex(offset), str( - data, encoding="latin-1", errors="?" + yield ( + format_hints.Hex(offset), + str(data, encoding="latin-1", errors="?"), ) def run(self): diff --git a/volatility3/framework/plugins/isfinfo.py b/volatility3/framework/plugins/isfinfo.py index 1c2ac52e9..10f391321 100644 --- a/volatility3/framework/plugins/isfinfo.py +++ b/volatility3/framework/plugins/isfinfo.py @@ -72,9 +72,12 @@ class IsfInfo(plugins.PluginInterface): for extension in constants.ISF_EXTENSIONS: # By ending with an extension (and therefore, not /), we should not return any directories if name.endswith(extension): - yield "jar:file:" + str( - pathlib.Path(base_name) - ) + "!" + name + yield ( + "jar:file:" + + str(pathlib.Path(base_name)) + + "!" + + name + ) else: for extension in constants.ISF_EXTENSIONS: diff --git a/volatility3/framework/plugins/linux/graphics/fbdev.py b/volatility3/framework/plugins/linux/graphics/fbdev.py index 1f9fd74b5..ebdbd1706 100644 --- a/volatility3/framework/plugins/linux/graphics/fbdev.py +++ b/volatility3/framework/plugins/linux/graphics/fbdev.py @@ -245,7 +245,6 @@ You can try using ffmpeg to decode the raw buffer. Example usage: return fb def _generator(self): - if not has_pil: vollog.error( "PIL (pillow) module is required to use this plugin. Please install it manually or through pyproject.toml." diff --git a/volatility3/framework/plugins/linux/ip.py b/volatility3/framework/plugins/linux/ip.py index 348f3c13f..164fe62dd 100644 --- a/volatility3/framework/plugins/linux/ip.py +++ b/volatility3/framework/plugins/linux/ip.py @@ -47,7 +47,17 @@ class Addr(plugins.PluginInterface): prefix_len = in_ifaddr.get_prefix_len() scope_type = in_ifaddr.get_scope_type() ip_addr = in_ifaddr.get_address() - yield net_ns_id, iface_ifindex, iface_name, mac_addr, promisc, ip_addr, prefix_len, scope_type, operational_state + yield ( + net_ns_id, + iface_ifindex, + iface_name, + mac_addr, + promisc, + ip_addr, + prefix_len, + scope_type, + operational_state, + ) # Interface IPv6 Addresses inet6_dev = net_dev.ip6_ptr.dereference().cast("inet6_dev") @@ -55,7 +65,17 @@ class Addr(plugins.PluginInterface): prefix_len = inet6_ifaddr.get_prefix_len() scope_type = inet6_ifaddr.get_scope_type() ip6_addr = inet6_ifaddr.get_address() - yield net_ns_id, iface_ifindex, iface_name, mac_addr, promisc, ip6_addr, prefix_len, scope_type, operational_state + yield ( + net_ns_id, + iface_ifindex, + iface_name, + mac_addr, + promisc, + ip6_addr, + prefix_len, + scope_type, + operational_state, + ) def _enumerate_net_namespace_list(self): vmlinux = self.context.modules[self.config["kernel"]] @@ -82,16 +102,19 @@ class Addr(plugins.PluginInterface): scope_type, operational_state, ) in self._gather_net_dev_info(net_dev): - yield 0, ( - net_ns_id or renderers.NotAvailableValue(), - iface_ifindex, - iface_name, - mac_addr, - promisc, - ip6_addr, - prefix_len, - scope_type, - operational_state, + yield ( + 0, + ( + net_ns_id or renderers.NotAvailableValue(), + iface_ifindex, + iface_name, + mac_addr, + promisc, + ip6_addr, + prefix_len, + scope_type, + operational_state, + ), ) def run(self): @@ -150,7 +173,16 @@ class Link(plugins.PluginInterface): ] flags_str = ",".join(flags_list) - yield net_ns_id or renderers.NotAvailableValue(), iface_name, mac_addr, operational_state, mtu, qdisc_name or renderers.NotAvailableValue(), qlen, flags_str + yield ( + net_ns_id or renderers.NotAvailableValue(), + iface_name, + mac_addr, + operational_state, + mtu, + qdisc_name or renderers.NotAvailableValue(), + qlen, + flags_str, + ) def _generator(self): vmlinux = self.context.modules[self.config["kernel"]] diff --git a/volatility3/framework/plugins/linux/kmsg.py b/volatility3/framework/plugins/linux/kmsg.py index 1069a312f..ba02763d0 100644 --- a/volatility3/framework/plugins/linux/kmsg.py +++ b/volatility3/framework/plugins/linux/kmsg.py @@ -551,12 +551,15 @@ class Kmsg(interfaces.plugins.PluginInterface): for facility, level, timestamp, caller, line in ABCKmsg.run_all( context=self.context, config=self.config ): - yield 0, ( - facility, - level, - timestamp, - caller or renderers.NotAvailableValue(), - line, + yield ( + 0, + ( + facility, + level, + timestamp, + caller or renderers.NotAvailableValue(), + line, + ), ) def run(self): diff --git a/volatility3/framework/plugins/linux/lsof.py b/volatility3/framework/plugins/linux/lsof.py index 283eabca0..be521750a 100644 --- a/volatility3/framework/plugins/linux/lsof.py +++ b/volatility3/framework/plugins/linux/lsof.py @@ -220,5 +220,9 @@ class Lsof(plugins.PluginInterface, timeliner.TimeLinerInterface): ) yield description, timeliner.TimeLinerType.CHANGED, fd_user.change_time - yield description, timeliner.TimeLinerType.MODIFIED, fd_user.modification_time + yield ( + description, + timeliner.TimeLinerType.MODIFIED, + fd_user.modification_time, + ) yield description, timeliner.TimeLinerType.ACCESSED, fd_user.access_time diff --git a/volatility3/framework/plugins/linux/malware/check_afinfo.py b/volatility3/framework/plugins/linux/malware/check_afinfo.py index 47da21615..4ecf5f788 100644 --- a/volatility3/framework/plugins/linux/malware/check_afinfo.py +++ b/volatility3/framework/plugins/linux/malware/check_afinfo.py @@ -3,6 +3,7 @@ # """A module containing a plugin that verifies the operation function pointers of network protocols.""" + import logging from typing import List, Tuple, Generator diff --git a/volatility3/framework/plugins/linux/malware/check_syscall.py b/volatility3/framework/plugins/linux/malware/check_syscall.py index 1188bf250..6476d6621 100644 --- a/volatility3/framework/plugins/linux/malware/check_syscall.py +++ b/volatility3/framework/plugins/linux/malware/check_syscall.py @@ -2,6 +2,7 @@ # which is available at https://www.volatilityfoundation.org/license/vsl-v1.0 # """A module containing a plugin that checks the system call table for hooks.""" + import contextlib import logging from typing import List diff --git a/volatility3/framework/plugins/linux/malware/netfilter.py b/volatility3/framework/plugins/linux/malware/netfilter.py index d724d4296..6bc5cd1bf 100644 --- a/volatility3/framework/plugins/linux/malware/netfilter.py +++ b/volatility3/framework/plugins/linux/malware/netfilter.py @@ -223,7 +223,16 @@ class AbstractNetfilter(ABC): ) hooked = module_info is None - yield netns, proto_name, hook_name, priority, hook_ops_hook, module_info, symbol_name, hooked + yield ( + netns, + proto_name, + hook_name, + priority, + hook_ops_hook, + module_info, + symbol_name, + hooked, + ) @classmethod @abstractmethod diff --git a/volatility3/framework/plugins/linux/malware/tty_check.py b/volatility3/framework/plugins/linux/malware/tty_check.py index 1547c5cc6..00e85f251 100644 --- a/volatility3/framework/plugins/linux/malware/tty_check.py +++ b/volatility3/framework/plugins/linux/malware/tty_check.py @@ -100,11 +100,14 @@ class Tty_Check(plugins.PluginInterface): else: module_name = renderers.NotAvailableValue() - yield 0, ( - name, - format_hints.Hex(recv_buf), - module_name, - symbol_name or renderers.NotAvailableValue(), + yield ( + 0, + ( + name, + format_hints.Hex(recv_buf), + module_name, + symbol_name or renderers.NotAvailableValue(), + ), ) def run(self): diff --git a/volatility3/framework/plugins/linux/module_extract.py b/volatility3/framework/plugins/linux/module_extract.py index 97824aca0..a8864281a 100644 --- a/volatility3/framework/plugins/linux/module_extract.py +++ b/volatility3/framework/plugins/linux/module_extract.py @@ -75,10 +75,13 @@ class ModuleExtract(interfaces.plugins.PluginInterface): with self.open(file_name) as file_handle: file_handle.write(elf_data) - yield 0, ( - format_hints.Hex(base_address), - len(elf_data), - file_handle.preferred_filename, + yield ( + 0, + ( + format_hints.Hex(base_address), + len(elf_data), + file_handle.preferred_filename, + ), ) def run(self): diff --git a/volatility3/framework/plugins/linux/pagecache.py b/volatility3/framework/plugins/linux/pagecache.py index 1fd96d5d2..2d20a2fb1 100644 --- a/volatility3/framework/plugins/linux/pagecache.py +++ b/volatility3/framework/plugins/linux/pagecache.py @@ -386,7 +386,11 @@ class Files(plugins.PluginInterface, timeliner.TimeLinerInterface): inode_out = inode_in.to_user(vmlinux_layer) description = f"Cached Inode for {inode_out.path}" yield description, timeliner.TimeLinerType.ACCESSED, inode_out.access_time - yield description, timeliner.TimeLinerType.MODIFIED, inode_out.modification_time + yield ( + description, + timeliner.TimeLinerType.MODIFIED, + inode_out.modification_time, + ) yield description, timeliner.TimeLinerType.CHANGED, inode_out.change_time @classmethod @@ -813,7 +817,6 @@ class RecoverFs(plugins.PluginInterface): visited_paths = seen_prefixes = set() for inode_in in inodes_iter: - # Code is slightly duplicated here with the if-block below. # However this prevents unneeded tar manipulation if fifo # or sock inodes come through for example. diff --git a/volatility3/framework/plugins/linux/pslist.py b/volatility3/framework/plugins/linux/pslist.py index 9b31976ec..71caa0853 100644 --- a/volatility3/framework/plugins/linux/pslist.py +++ b/volatility3/framework/plugins/linux/pslist.py @@ -225,18 +225,21 @@ class PsList(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface): task_euid = self._format_cred(task_fields.euid) task_egid = self._format_cred(task_fields.egid) - yield 0, ( - format_hints.Hex(task_fields.offset), - task_fields.user_pid, - task_fields.user_tid, - task_fields.user_ppid, - task_fields.name, - task_uid, - task_gid, - task_euid, - task_egid, - task_fields.creation_time or renderers.NotAvailableValue(), - file_output, + yield ( + 0, + ( + format_hints.Hex(task_fields.offset), + task_fields.user_pid, + task_fields.user_tid, + task_fields.user_ppid, + task_fields.name, + task_uid, + task_gid, + task_euid, + task_egid, + task_fields.creation_time or renderers.NotAvailableValue(), + file_output, + ), ) @classmethod diff --git a/volatility3/framework/plugins/linux/tracing/perf_events.py b/volatility3/framework/plugins/linux/tracing/perf_events.py index 23b2f2c72..ff922784d 100644 --- a/volatility3/framework/plugins/linux/tracing/perf_events.py +++ b/volatility3/framework/plugins/linux/tracing/perf_events.py @@ -70,7 +70,6 @@ class PerfEvents(plugins.PluginInterface): for task in pslist.PsList.list_tasks( context, vmlinux_module_name, include_threads=True ): - # walk the list of perf_event entries for this process for event in task.perf_event_list.to_list( vmlinux.symbol_table_name + constants.BANG + "perf_event", "owner_entry" diff --git a/volatility3/framework/plugins/linux/vmaregexscan.py b/volatility3/framework/plugins/linux/vmaregexscan.py index fb757beb5..2fde3aadc 100644 --- a/volatility3/framework/plugins/linux/vmaregexscan.py +++ b/volatility3/framework/plugins/linux/vmaregexscan.py @@ -64,7 +64,6 @@ class VmaRegExScan(plugins.PluginInterface): vollog.debug(f"RegEx Pattern: {regex_pattern}") for task in tasks: - if not task.mm: continue name = utility.array_to_string(task.comm) @@ -106,12 +105,15 @@ class VmaRegExScan(plugins.PluginInterface): bytes_result = result_data user_pid = task.tgid - yield 0, ( - user_pid, - name, - format_hints.Hex(offset), - text_result, - bytes_result, + yield ( + 0, + ( + user_pid, + name, + format_hints.Hex(offset), + text_result, + bytes_result, + ), ) def run(self): diff --git a/volatility3/framework/plugins/linux/vmayarascan.py b/volatility3/framework/plugins/linux/vmayarascan.py index 64f5827c1..d9466f512 100644 --- a/volatility3/framework/plugins/linux/vmayarascan.py +++ b/volatility3/framework/plugins/linux/vmayarascan.py @@ -103,12 +103,15 @@ class VmaYaraScan(interfaces.plugins.PluginInterface): layer_name=proc_layer.name, length=len(value), ) - yield 0, ( - format_hints.Hex(offset), - task.tgid, - rule_name, - name, - layer_data, + yield ( + 0, + ( + format_hints.Hex(offset), + task.tgid, + rule_name, + name, + layer_data, + ), ) @classmethod diff --git a/volatility3/framework/plugins/mac/lsmod.py b/volatility3/framework/plugins/mac/lsmod.py index c6f57f889..05a0ee72f 100644 --- a/volatility3/framework/plugins/mac/lsmod.py +++ b/volatility3/framework/plugins/mac/lsmod.py @@ -3,6 +3,7 @@ # """A module containing a collection of plugins that produce data typically found in Mac's lsmod command.""" + from typing import Set from volatility3.framework import renderers, interfaces, exceptions diff --git a/volatility3/framework/plugins/mac/mount.py b/volatility3/framework/plugins/mac/mount.py index 0f3aa745c..3d9dcd916 100644 --- a/volatility3/framework/plugins/mac/mount.py +++ b/volatility3/framework/plugins/mac/mount.py @@ -3,6 +3,7 @@ # """A module containing a collection of plugins that produce data typically found in Mac's mount command.""" + from volatility3.framework import renderers, interfaces from volatility3.framework.configuration import requirements from volatility3.framework.interfaces import plugins diff --git a/volatility3/framework/plugins/mac/psaux.py b/volatility3/framework/plugins/mac/psaux.py index ba9b7b5f6..bdcfb1466 100644 --- a/volatility3/framework/plugins/mac/psaux.py +++ b/volatility3/framework/plugins/mac/psaux.py @@ -2,6 +2,7 @@ # which is available at https://www.volatilityfoundation.org/license/vsl-v1.0 # """In-memory artifacts from OSX systems.""" + from typing import Iterator, Tuple, Any, Generator, List from volatility3.framework import exceptions, renderers, interfaces diff --git a/volatility3/framework/plugins/windows/callbacks.py b/volatility3/framework/plugins/windows/callbacks.py index b8c9fe751..2a65f76bb 100644 --- a/volatility3/framework/plugins/windows/callbacks.py +++ b/volatility3/framework/plugins/windows/callbacks.py @@ -78,7 +78,6 @@ class Callbacks(interfaces.plugins.PluginInterface): def _create_default_scan_constraints( context: interfaces.context.ContextInterface, symbol_table: str ) -> List[poolscanner.PoolConstraint]: - shutdown_packet_size = context.symbol_space.get_type( symbol_table + constants.BANG + "_SHUTDOWN_PACKET" ).size @@ -590,7 +589,11 @@ class Callbacks(interfaces.plugins.PluginInterface): except exceptions.InvalidAddressException: component = renderers.UnreadableValue() - yield "KeBugCheckReasonCallbackListHead", callback.CallbackRoutine, component + yield ( + "KeBugCheckReasonCallbackListHead", + callback.CallbackRoutine, + component, + ) @classmethod def list_bugcheck_callbacks( diff --git a/volatility3/framework/plugins/windows/deskscan.py b/volatility3/framework/plugins/windows/deskscan.py index 35430be5d..202eaf330 100644 --- a/volatility3/framework/plugins/windows/deskscan.py +++ b/volatility3/framework/plugins/windows/deskscan.py @@ -77,6 +77,11 @@ class DeskScan(desktops.Desktops): continue for _thread, process_name, process_pid in desktop.get_threads(): - yield format_hints.Hex( - desktop.vol.offset - ), winsta_name, session_id, desktop_name, process_name, process_pid + yield ( + format_hints.Hex(desktop.vol.offset), + winsta_name, + session_id, + desktop_name, + process_name, + process_pid, + ) diff --git a/volatility3/framework/plugins/windows/desktops.py b/volatility3/framework/plugins/windows/desktops.py index c6557085e..f2a985c6b 100644 --- a/volatility3/framework/plugins/windows/desktops.py +++ b/volatility3/framework/plugins/windows/desktops.py @@ -63,9 +63,14 @@ class Desktops(interfaces.plugins.PluginInterface): for desktop, desktop_name in winsta.desktops(kernel.symbol_table_name): # for each desktop, walk its threads for _thread, process_name, process_pid in desktop.get_threads(): - yield format_hints.Hex( - desktop.vol.offset - ), station_name, session_id, desktop_name, process_name, process_pid + yield ( + format_hints.Hex(desktop.vol.offset), + station_name, + session_id, + desktop_name, + process_name, + process_pid, + ) def _generator(self): kernel_name = self.config["kernel"] diff --git a/volatility3/framework/plugins/windows/kpcrs.py b/volatility3/framework/plugins/windows/kpcrs.py index 213e3833c..d544fd498 100644 --- a/volatility3/framework/plugins/windows/kpcrs.py +++ b/volatility3/framework/plugins/windows/kpcrs.py @@ -96,7 +96,6 @@ class KPCRs(interfaces.plugins.PluginInterface): yield kpcr, kpcr.member(kpcr_member) def _generator(self) -> Iterator[Tuple]: - for kpcr, current_prcb in self.list_kpcrs(self.context, self.config["kernel"]): yield ( 0, diff --git a/volatility3/framework/plugins/windows/malware/direct_system_calls.py b/volatility3/framework/plugins/windows/malware/direct_system_calls.py index dce09605b..f6b9e53bb 100644 --- a/volatility3/framework/plugins/windows/malware/direct_system_calls.py +++ b/volatility3/framework/plugins/windows/malware/direct_system_calls.py @@ -451,12 +451,15 @@ class DirectSystemCalls(interfaces.plugins.PluginInterface): address, disasm_bytes = syscall_info - yield 0, ( - proc_name, - proc.UniqueProcessId, - vad_path, - format_hints.Hex(address), - disasm_bytes, + yield ( + 0, + ( + proc_name, + proc.UniqueProcessId, + vad_path, + format_hints.Hex(address), + disasm_bytes, + ), ) def run(self) -> renderers.TreeGrid: diff --git a/volatility3/framework/plugins/windows/malware/hollowprocesses.py b/volatility3/framework/plugins/windows/malware/hollowprocesses.py index f981ae340..1ea46b540 100644 --- a/volatility3/framework/plugins/windows/malware/hollowprocesses.py +++ b/volatility3/framework/plugins/windows/malware/hollowprocesses.py @@ -198,10 +198,13 @@ class HollowProcesses(interfaces.plugins.PluginInterface): for check in checks: for note in check(proc, vads, dlls): - yield 0, ( - pid, - proc_name, - note, + yield ( + 0, + ( + pid, + proc_name, + note, + ), ) def run(self): diff --git a/volatility3/framework/plugins/windows/malware/malfind.py b/volatility3/framework/plugins/windows/malware/malfind.py index 01da93e1f..2f6d9eb56 100644 --- a/volatility3/framework/plugins/windows/malware/malfind.py +++ b/volatility3/framework/plugins/windows/malware/malfind.py @@ -92,8 +92,11 @@ class Malfind(interfaces.plugins.PluginInterface): 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 + yield ( + vad, + data_object.context.layers[data_object.layer_name].read( + data_object.offset, data_object.length + ), ) @classmethod diff --git a/volatility3/framework/plugins/windows/malware/pebmasquerade.py b/volatility3/framework/plugins/windows/malware/pebmasquerade.py index dd85fb570..cd898239f 100644 --- a/volatility3/framework/plugins/windows/malware/pebmasquerade.py +++ b/volatility3/framework/plugins/windows/malware/pebmasquerade.py @@ -167,7 +167,6 @@ class PebMasquerade(interfaces.plugins.PluginInterface): if isinstance(peb_imagefilepath, str) and peb: try: - # Length values are of type USHORT peb_imagefilepath_length = ( peb.ProcessParameters.ImagePathName.Length // 2 diff --git a/volatility3/framework/plugins/windows/malware/processghosting.py b/volatility3/framework/plugins/windows/malware/processghosting.py index f234bc2e7..fb8ec527d 100644 --- a/volatility3/framework/plugins/windows/malware/processghosting.py +++ b/volatility3/framework/plugins/windows/malware/processghosting.py @@ -149,9 +149,12 @@ class ProcessGhosting(interfaces.plugins.PluginInterface): for file_object_address, delete_pending, delete_on_close in cls._vad_checks( control_area, path ): - yield format_hints.Hex( - file_object_address - ), delete_pending, delete_on_close, vad_base + yield ( + format_hints.Hex(file_object_address), + delete_pending, + delete_on_close, + vad_base, + ) def _generator(self, procs): kernel = self.context.modules[self.config["kernel"]] @@ -187,14 +190,17 @@ class ProcessGhosting(interfaces.plugins.PluginInterface): else: path = renderers.NotAvailableValue() - yield 0, ( - pid, - process_name, - format_hints.Hex(base_address), - format_hints.Hex(file_object_address), - delete_pending or renderers.NotApplicableValue(), - delete_on_close or renderers.NotApplicableValue(), - path, + yield ( + 0, + ( + pid, + process_name, + format_hints.Hex(base_address), + format_hints.Hex(file_object_address), + delete_pending or renderers.NotApplicableValue(), + delete_on_close or renderers.NotApplicableValue(), + path, + ), ) def run(self): diff --git a/volatility3/framework/plugins/windows/malware/skeleton_key_check.py b/volatility3/framework/plugins/windows/malware/skeleton_key_check.py index d9cba0704..10c6222bc 100644 --- a/volatility3/framework/plugins/windows/malware/skeleton_key_check.py +++ b/volatility3/framework/plugins/windows/malware/skeleton_key_check.py @@ -648,12 +648,15 @@ class Skeleton_Key_Check(interfaces.plugins.PluginInterface): csystem, cryptdll_base, cryptdll_size ) - yield 0, ( - lsass_proc.UniqueProcessId, - "lsass.exe", - skeleton_key_present, - format_hints.Hex(csystem.Initialize), - format_hints.Hex(csystem.Decrypt), + yield ( + 0, + ( + lsass_proc.UniqueProcessId, + "lsass.exe", + skeleton_key_present, + format_hints.Hex(csystem.Initialize), + format_hints.Hex(csystem.Decrypt), + ), ) def _lsass_proc_filter(self, proc): diff --git a/volatility3/framework/plugins/windows/malware/suspicious_threads.py b/volatility3/framework/plugins/windows/malware/suspicious_threads.py index 3da8cb21a..803a0b04b 100644 --- a/volatility3/framework/plugins/windows/malware/suspicious_threads.py +++ b/volatility3/framework/plugins/windows/malware/suspicious_threads.py @@ -196,14 +196,17 @@ class SuspiciousThreads(interfaces.plugins.PluginInterface): for vad_path, note in self._check_thread_address( exe_path, ranges, address ): - yield 0, ( - proc_name, - pid, - tid, - context, - format_hints.Hex(address), - vad_path, - note, + yield ( + 0, + ( + proc_name, + pid, + tid, + context, + format_hints.Hex(address), + vad_path, + note, + ), ) def run(self): diff --git a/volatility3/framework/plugins/windows/mftscan.py b/volatility3/framework/plugins/windows/mftscan.py index f0be3cf7f..2dfecb93b 100644 --- a/volatility3/framework/plugins/windows/mftscan.py +++ b/volatility3/framework/plugins/windows/mftscan.py @@ -132,19 +132,22 @@ class MFTScan(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface): # There should only be one STANDARD_INFORMATION attribute, but we # do this just in case. for std_information in mft_record.standard_information_entries(): - yield 0, cls.MFTScanResult( - format_hints.Hex(std_information.vol.offset), - str(mft_record.get_signature()), - mft_record.RecordNumber, - mft_record.LinkCount, - mft_flag, - renderers.NotApplicableValue(), - "STANDARD_INFORMATION", - conversion.wintime_to_datetime(std_information.CreationTime), - conversion.wintime_to_datetime(std_information.ModifiedTime), - conversion.wintime_to_datetime(std_information.UpdatedTime), - conversion.wintime_to_datetime(std_information.AccessedTime), - renderers.NotApplicableValue(), + yield ( + 0, + cls.MFTScanResult( + format_hints.Hex(std_information.vol.offset), + str(mft_record.get_signature()), + mft_record.RecordNumber, + mft_record.LinkCount, + mft_flag, + renderers.NotApplicableValue(), + "STANDARD_INFORMATION", + conversion.wintime_to_datetime(std_information.CreationTime), + conversion.wintime_to_datetime(std_information.ModifiedTime), + conversion.wintime_to_datetime(std_information.UpdatedTime), + conversion.wintime_to_datetime(std_information.AccessedTime), + renderers.NotApplicableValue(), + ), ) except exceptions.InvalidAddressException: pass @@ -163,26 +166,28 @@ class MFTScan(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface): # File Name Attribute try: for filename_info in mft_record.filename_entries(): - # If we don't have a valid enum, coerce to hex so we can keep the record try: permissions = filename_info.Flags.lookup() except ValueError: permissions = hex(filename_info.Flags) - yield 1, cls.MFTScanResult( - format_hints.Hex(filename_info.vol.offset), - str(mft_record.get_signature()), - mft_record.RecordNumber, - mft_record.LinkCount, - mft_flag, - permissions, - "FILE_NAME", - conversion.wintime_to_datetime(filename_info.CreationTime), - conversion.wintime_to_datetime(filename_info.ModifiedTime), - conversion.wintime_to_datetime(filename_info.UpdatedTime), - conversion.wintime_to_datetime(filename_info.AccessedTime), - filename_info.get_full_name(), + yield ( + 1, + cls.MFTScanResult( + format_hints.Hex(filename_info.vol.offset), + str(mft_record.get_signature()), + mft_record.RecordNumber, + mft_record.LinkCount, + mft_flag, + permissions, + "FILE_NAME", + conversion.wintime_to_datetime(filename_info.CreationTime), + conversion.wintime_to_datetime(filename_info.ModifiedTime), + conversion.wintime_to_datetime(filename_info.UpdatedTime), + conversion.wintime_to_datetime(filename_info.AccessedTime), + filename_info.get_full_name(), + ), ) except exceptions.InvalidAddressException: return @@ -214,22 +219,25 @@ class MFTScan(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface): # but in this case memory usage is so extreme due to the number of # records that it becomes necessary. The rich types are still # exposed through classmethods. - yield level, ( - record.offset, - record.record_type, - int(record.record_number), - int(record.link_count), - record.mft_type, - record.permissions, - record.attribute_type, - record.created, - record.modified, - record.updated, - record.accessed, + yield ( + level, ( - str(record.filename) - if isinstance(record.filename, objects.String) - else record.filename + record.offset, + record.record_type, + int(record.record_number), + int(record.link_count), + record.mft_type, + record.permissions, + record.attribute_type, + record.created, + record.modified, + record.updated, + record.accessed, + ( + str(record.filename) + if isinstance(record.filename, objects.String) + else record.filename + ), ), ) @@ -344,22 +352,25 @@ class ADS(interfaces.plugins.PluginInterface): # but in this case memory usage is so extreme due to the number of # records that it becomes necessary. The rich types are still # exposed through classmethods. - yield 0, ( - record.offset, - str(record.signature), - int(record.record_number), - record.attribute_type, + yield ( + 0, ( - str(record.filename) - if isinstance(record.filename, objects.String) - else record.filename + record.offset, + str(record.signature), + int(record.record_number), + record.attribute_type, + ( + str(record.filename) + if isinstance(record.filename, objects.String) + else record.filename + ), + ( + str(record.stream_name) + if isinstance(record.stream_name, objects.String) + else record.stream_name + ), + record.content, ), - ( - str(record.stream_name) - if isinstance(record.stream_name, objects.String) - else record.stream_name - ), - record.content, ) def run(self): @@ -454,13 +465,16 @@ class ResidentData(interfaces.plugins.PluginInterface): # but in this case memory usage is so extreme due to the number of # records that it becomes necessary. The rich types are still # exposed through classmethods. - yield 0, ( - resident_data_entry.offset, - str(resident_data_entry.signature), - int(resident_data_entry.record_number), - resident_data_entry.attribute_type, - str(resident_data_entry.filename), - resident_data_entry.content, + yield ( + 0, + ( + resident_data_entry.offset, + str(resident_data_entry.signature), + int(resident_data_entry.record_number), + resident_data_entry.attribute_type, + str(resident_data_entry.filename), + resident_data_entry.content, + ), ) def run(self): diff --git a/volatility3/framework/plugins/windows/modules.py b/volatility3/framework/plugins/windows/modules.py index 1ec965737..18a7b5919 100644 --- a/volatility3/framework/plugins/windows/modules.py +++ b/volatility3/framework/plugins/windows/modules.py @@ -119,13 +119,16 @@ class Modules(interfaces.plugins.PluginInterface): if self.config["dump"]: file_output = self.dump_module(session_layers, pe_table_name, mod) - yield 0, ( - format_hints.Hex(mod.vol.offset), - format_hints.Hex(mod.DllBase), - format_hints.Hex(mod.SizeOfImage), - BaseDllName, - FullDllName, - file_output, + yield ( + 0, + ( + format_hints.Hex(mod.vol.offset), + format_hints.Hex(mod.DllBase), + format_hints.Hex(mod.SizeOfImage), + BaseDllName, + FullDllName, + file_output, + ), ) @classmethod diff --git a/volatility3/framework/plugins/windows/poolscanner.py b/volatility3/framework/plugins/windows/poolscanner.py index 157282ce1..43dcd0482 100644 --- a/volatility3/framework/plugins/windows/poolscanner.py +++ b/volatility3/framework/plugins/windows/poolscanner.py @@ -436,7 +436,6 @@ class PoolScanner(plugins.PluginInterface): constraints, alignment=alignment, ): - mem_objects = header.get_object( constraint=constraint, use_top_down=is_windows_8_or_later, diff --git a/volatility3/framework/plugins/windows/registry/amcache.py b/volatility3/framework/plugins/windows/registry/amcache.py index a6ad1848e..ed078993c 100644 --- a/volatility3/framework/plugins/windows/registry/amcache.py +++ b/volatility3/framework/plugins/windows/registry/amcache.py @@ -246,13 +246,29 @@ class Amcache(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface): ) -> Iterator[Tuple[str, timeliner.TimeLinerType, datetime.datetime]]: for _, entry in self._generator(): if isinstance(entry.last_modify_time, datetime.datetime): - yield f"Amcache: {entry.entry_type} {entry.path} registry key modified", timeliner.TimeLinerType.MODIFIED, entry.last_modify_time + yield ( + f"Amcache: {entry.entry_type} {entry.path} registry key modified", + timeliner.TimeLinerType.MODIFIED, + entry.last_modify_time, + ) if isinstance(entry.last_modify_time_2, datetime.datetime): - yield f"Amcache: {entry.entry_type} {entry.path} STANDARD_INFORMATION create time", timeliner.TimeLinerType.CREATED, entry.last_modify_time_2 + yield ( + f"Amcache: {entry.entry_type} {entry.path} STANDARD_INFORMATION create time", + timeliner.TimeLinerType.CREATED, + entry.last_modify_time_2, + ) if isinstance(entry.install_time, datetime.datetime): - yield f"Amcache: {entry.entry_type} {entry.path} installed", timeliner.TimeLinerType.CREATED, entry.install_time + yield ( + f"Amcache: {entry.entry_type} {entry.path} installed", + timeliner.TimeLinerType.CREATED, + entry.install_time, + ) if isinstance(entry.compile_time, datetime.datetime): - yield f"Amcache: {entry.entry_type} {entry.path} compiled (PE metadata)", timeliner.TimeLinerType.MODIFIED, entry.compile_time + yield ( + f"Amcache: {entry.entry_type} {entry.path} compiled (PE metadata)", + timeliner.TimeLinerType.MODIFIED, + entry.compile_time, + ) @classmethod def get_amcache_hive( @@ -319,20 +335,23 @@ class Amcache(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface): vollog.debug(f"Found sha1hash {sha1_hash}") product_name = _get_string_value(values, val_enum.Product.value) - yield program_id, _AmcacheEntry( - AmcacheEntryType.File.name, - path=path, - company=company, - last_modify_time=last_mod_time, - last_modify_time_2=last_mod_time_2, - install_time=install_time, - compile_time=compile_time, - sha1_hash=( - sha1_hash.lstrip("0000") - if isinstance(sha1_hash, str) - else sha1_hash + yield ( + program_id, + _AmcacheEntry( + AmcacheEntryType.File.name, + path=path, + company=company, + last_modify_time=last_mod_time, + last_modify_time_2=last_mod_time_2, + install_time=install_time, + compile_time=compile_time, + sha1_hash=( + sha1_hash.lstrip("0000") + if isinstance(sha1_hash, str) + else sha1_hash + ), + product_name=product_name, ), - product_name=product_name, ) @classmethod @@ -365,15 +384,18 @@ class Amcache(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface): ) version = _get_string_value(values, val_enum.Version.value) - yield program_id, _AmcacheEntry( - AmcacheEntryType.Program.name, - company=company, - last_modify_time=conversion.wintime_to_datetime( - program_key.LastWriteTime.QuadPart + yield ( + program_id, + _AmcacheEntry( + AmcacheEntryType.Program.name, + company=company, + last_modify_time=conversion.wintime_to_datetime( + program_key.LastWriteTime.QuadPart + ), + install_time=install_time, + product_name=product, + product_version=version, ), - install_time=install_time, - product_name=product, - product_version=version, ) @classmethod @@ -411,14 +433,17 @@ class Amcache(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface): product: str = name if isinstance(name, str) else "UNKNOWN" # type: ignore - yield program_id.strip().strip("\u0000"), _AmcacheEntry( - AmcacheEntryType.Program.name, - path=path, - last_modify_time=last_mod, - install_time=install_date, - product_name=product, - company=publisher, - product_version=version, + yield ( + program_id.strip().strip("\u0000"), + _AmcacheEntry( + AmcacheEntryType.Program.name, + path=path, + last_modify_time=last_mod, + install_time=install_date, + product_name=product, + company=publisher, + product_version=version, + ), ) @classmethod @@ -456,19 +481,22 @@ class Amcache(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface): prod_ver = _get_string_value(values, val_enum.ProductVersion.value) program_id = _get_string_value(values, val_enum.ProgramID.value) - yield program_id, _AmcacheEntry( - AmcacheEntryType.File.name, - path=path, - company=publisher, - last_modify_time=last_mod, - compile_time=linkdate, - sha1_hash=( - sha1_hash.lstrip("0000") - if isinstance(sha1_hash, str) - else sha1_hash + yield ( + program_id, + _AmcacheEntry( + AmcacheEntryType.File.name, + path=path, + company=publisher, + last_modify_time=last_mod, + compile_time=linkdate, + sha1_hash=( + sha1_hash.lstrip("0000") + if isinstance(sha1_hash, str) + else sha1_hash + ), + product_name=prod_name, + product_version=prod_ver, ), - product_name=prod_name, - product_version=prod_ver, ) @classmethod @@ -485,7 +513,6 @@ class Amcache(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface): wanted_values = [key.value for key in val_enum] for binary_key in driver_binary_key.get_subkeys(): - values = { str(value.get_name()): value for value in binary_key.get_values() @@ -636,7 +663,6 @@ class Amcache(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface): yield 0, empty_program def run(self): - return renderers.TreeGrid( [ ("EntryType", str), diff --git a/volatility3/framework/plugins/windows/registry/scheduled_tasks.py b/volatility3/framework/plugins/windows/registry/scheduled_tasks.py index 2c660229b..a3e5fabe2 100644 --- a/volatility3/framework/plugins/windows/registry/scheduled_tasks.py +++ b/volatility3/framework/plugins/windows/registry/scheduled_tasks.py @@ -185,7 +185,6 @@ NULL = "\u0000" class _ScheduledTasksReader(io.BytesIO): - def read_task_scheduler_time(self) -> Optional[datetime.datetime]: _ = bool(self.read_aligned_u1()) # is_localized filetime = self.decode_filetime() @@ -393,7 +392,6 @@ class TaskAction: num_attachment_filenames = reader.read_u4() if num_attachment_filenames is not None: - attachment_filenames = [ reader.read_bstring() for _ in range(num_attachment_filenames) ] @@ -1138,11 +1136,23 @@ class ScheduledTasks(interfaces.plugins.PluginInterface, timeliner.TimeLinerInte ) -> Iterator[Tuple[str, timeliner.TimeLinerType, datetime.datetime]]: for _, task in self._generator(): if isinstance(task.last_run_time, datetime.datetime): - yield f"ScheduledTasks: task action {task.action_description} with trigger {task.trigger_description} ran", timeliner.TimeLinerType.ACCESSED, task.last_run_time + yield ( + f"ScheduledTasks: task action {task.action_description} with trigger {task.trigger_description} ran", + timeliner.TimeLinerType.ACCESSED, + task.last_run_time, + ) if isinstance(task.last_successful_run_time, datetime.datetime): - yield f"ScheduledTasks: task action {task.action_description} with trigger {task.trigger_description} ran successfully", timeliner.TimeLinerType.ACCESSED, task.last_successful_run_time + yield ( + f"ScheduledTasks: task action {task.action_description} with trigger {task.trigger_description} ran successfully", + timeliner.TimeLinerType.ACCESSED, + task.last_successful_run_time, + ) if isinstance(task.creation_time, datetime.datetime): - yield f"ScheduledTasks: Creation Time for task {task.guid} with trigger {task.trigger_description or ''}", timeliner.TimeLinerType.CREATED, task.creation_time + yield ( + f"ScheduledTasks: Creation Time for task {task.guid} with trigger {task.trigger_description or ''}", + timeliner.TimeLinerType.CREATED, + task.creation_time, + ) @classmethod def get_software_hive( @@ -1203,7 +1213,6 @@ class ScheduledTasks(interfaces.plugins.PluginInterface, timeliner.TimeLinerInte def parse_dynamic_info_value( cls, dyn_info_value: reg_extensions.CM_KEY_VALUE ) -> Optional[DynamicInfo]: - try: data = dyn_info_value.decode_data() except exceptions.InvalidAddressException: @@ -1318,7 +1327,6 @@ class ScheduledTasks(interfaces.plugins.PluginInterface, timeliner.TimeLinerInte all_actions = action_set.actions or [None] if action_set is not None else [None] for action, trigger in itertools.product(all_actions, all_triggers): - if action is not None: if action.action_type in ( ActionType.Exe, diff --git a/volatility3/framework/plugins/windows/sessions.py b/volatility3/framework/plugins/windows/sessions.py index 29d0b2104..158820529 100644 --- a/volatility3/framework/plugins/windows/sessions.py +++ b/volatility3/framework/plugins/windows/sessions.py @@ -95,13 +95,16 @@ class Sessions(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface) # Group and yield each row for rows in sessions.values(): for row in rows: - yield 0, ( - row.get("session_id"), - row.get("session_type"), - row.get("process_id"), - row.get("process_name"), - row.get("user_name"), - row.get("process_start"), + yield ( + 0, + ( + row.get("session_id"), + row.get("session_type"), + row.get("process_id"), + row.get("process_name"), + row.get("user_name"), + row.get("process_start"), + ), ) def generate_timeline(self): diff --git a/volatility3/framework/plugins/windows/shimcachemem.py b/volatility3/framework/plugins/windows/shimcachemem.py index 7a03ebbb6..8935757d4 100644 --- a/volatility3/framework/plugins/windows/shimcachemem.py +++ b/volatility3/framework/plugins/windows/shimcachemem.py @@ -51,9 +51,17 @@ class ShimcacheMem(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterf ) -> Iterator[Tuple[str, timeliner.TimeLinerType, datetime]]: for _, (_, last_modified, last_update, _, _, file_path) in self._generator(): if isinstance(last_update, datetime): - yield f"Shimcache: File {file_path} executed", timeliner.TimeLinerType.ACCESSED, last_update + yield ( + f"Shimcache: File {file_path} executed", + timeliner.TimeLinerType.ACCESSED, + last_update, + ) if isinstance(last_modified, datetime): - yield f"Shimcache: File {file_path} modified", timeliner.TimeLinerType.MODIFIED, last_modified + yield ( + f"Shimcache: File {file_path} modified", + timeliner.TimeLinerType.MODIFIED, + last_modified, + ) @classmethod def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]: @@ -161,7 +169,7 @@ class ShimcacheMem(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterf continue try: - if proc_layer.read(vad.get_start(), 4) != b"\xEF\xBE\xAD\xDE": + if proc_layer.read(vad.get_start(), 4) != b"\xef\xbe\xad\xde": if pid == 624: vollog.debug("VAD magic bytes don't match DEADBEEF") continue diff --git a/volatility3/framework/plugins/windows/svcscan.py b/volatility3/framework/plugins/windows/svcscan.py index 5f0e4761e..1bfb98fda 100644 --- a/volatility3/framework/plugins/windows/svcscan.py +++ b/volatility3/framework/plugins/windows/svcscan.py @@ -150,7 +150,6 @@ class SvcScan(interfaces.plugins.PluginInterface): def _get_service_key( context, config_path: str, kernel_module_name: str ) -> Optional[objects.StructType]: - for hive in hivelist.HiveList.list_hives( context=context, base_config_path=interfaces.configuration.path_join( diff --git a/volatility3/framework/plugins/windows/thrdscan.py b/volatility3/framework/plugins/windows/thrdscan.py index 082b82284..1588f292e 100644 --- a/volatility3/framework/plugins/windows/thrdscan.py +++ b/volatility3/framework/plugins/windows/thrdscan.py @@ -167,16 +167,19 @@ class ThrdScan(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface) info = self.gather_thread_info(ethread, vads_cache) if info: - yield 0, ( - format_hints.Hex(info.offset), - info.pid, - info.tid, - format_hints.Hex(info.start_addr), - info.start_path or renderers.NotAvailableValue(), - format_hints.Hex(info.win32_start_addr), - info.win32_start_path or renderers.NotAvailableValue(), - info.create_time, - info.exit_time, + yield ( + 0, + ( + format_hints.Hex(info.offset), + info.pid, + info.tid, + format_hints.Hex(info.start_addr), + info.start_path or renderers.NotAvailableValue(), + format_hints.Hex(info.win32_start_addr), + info.win32_start_path or renderers.NotAvailableValue(), + info.create_time, + info.exit_time, + ), ) def generate_timeline(self): diff --git a/volatility3/framework/plugins/windows/vadregexscan.py b/volatility3/framework/plugins/windows/vadregexscan.py index 068838e35..6a3cc394f 100644 --- a/volatility3/framework/plugins/windows/vadregexscan.py +++ b/volatility3/framework/plugins/windows/vadregexscan.py @@ -62,7 +62,6 @@ class VadRegExScan(plugins.PluginInterface): vollog.debug(f"RegEx Pattern: {regex_pattern}") for proc in procs: - # attempt to create a process layer for each proc proc_layer_name = proc.add_process_layer() if not proc_layer_name: @@ -106,12 +105,15 @@ class VadRegExScan(plugins.PluginInterface): max_length=proc.ImageFileName.vol.count, errors="replace", ) - yield 0, ( - proc_id, - process_name, - format_hints.Hex(offset), - text_result, - bytes_result, + yield ( + 0, + ( + proc_id, + process_name, + format_hints.Hex(offset), + text_result, + bytes_result, + ), ) def run(self): diff --git a/volatility3/framework/plugins/windows/vadyarascan.py b/volatility3/framework/plugins/windows/vadyarascan.py index bbbe49f2d..9a38213fa 100644 --- a/volatility3/framework/plugins/windows/vadyarascan.py +++ b/volatility3/framework/plugins/windows/vadyarascan.py @@ -100,21 +100,24 @@ class VadYaraScan(interfaces.plugins.PluginInterface): layer_name=layer.name, length=len(value), ) - yield 0, ( - format_hints.Hex(offset), - task.UniqueProcessId, - task.get_create_time(), - task.InheritedFromUniqueProcessId, - task.ImageFileName.cast( - "string", - max_length=task.ImageFileName.vol.count, - errors="replace", + yield ( + 0, + ( + format_hints.Hex(offset), + task.UniqueProcessId, + task.get_create_time(), + task.InheritedFromUniqueProcessId, + task.ImageFileName.cast( + "string", + max_length=task.ImageFileName.vol.count, + errors="replace", + ), + task.get_session_id(), + task.ActiveThreads, + rule_name, + name, + layer_data, ), - task.get_session_id(), - task.ActiveThreads, - rule_name, - name, - layer_data, ) @classmethod diff --git a/volatility3/framework/plugins/windows/windows.py b/volatility3/framework/plugins/windows/windows.py index 9d4317df9..c98869665 100644 --- a/volatility3/framework/plugins/windows/windows.py +++ b/volatility3/framework/plugins/windows/windows.py @@ -112,15 +112,18 @@ class Windows(interfaces.plugins.PluginInterface): ) continue - yield 0, ( - format_hints.Hex(window.vol.offset), - station_name, - sess_id, - desktop_name, - window_name or renderers.NotAvailableValue(), - window_proc, - process_name, - process_pid, + yield ( + 0, + ( + format_hints.Hex(window.vol.offset), + station_name, + sess_id, + desktop_name, + window_name or renderers.NotAvailableValue(), + window_proc, + process_name, + process_pid, + ), ) def run(self): diff --git a/volatility3/framework/renderers/__init__.py b/volatility3/framework/renderers/__init__.py index 8732e6e88..899c19196 100644 --- a/volatility3/framework/renderers/__init__.py +++ b/volatility3/framework/renderers/__init__.py @@ -6,6 +6,7 @@ Renderers display the unified output format in some manner (be it text or file or graphical output """ + import collections import collections.abc import dataclasses diff --git a/volatility3/framework/renderers/format_hints.py b/volatility3/framework/renderers/format_hints.py index d57c7e9f1..83f36a1a0 100644 --- a/volatility3/framework/renderers/format_hints.py +++ b/volatility3/framework/renderers/format_hints.py @@ -8,6 +8,7 @@ These hints allow a plugin to indicate how they would like data from a particula Text renderers should attempt to honour all hints provided in this module where possible """ + from typing import Type, Union from volatility3.framework import interfaces diff --git a/volatility3/framework/symbols/__init__.py b/volatility3/framework/symbols/__init__.py index 87f2288d7..a050298ba 100644 --- a/volatility3/framework/symbols/__init__.py +++ b/volatility3/framework/symbols/__init__.py @@ -210,9 +210,10 @@ class SymbolSpace(interfaces.symbols.SymbolSpaceInterface): replacements = set() # Whole Symbols that still need traversing while traverse_list: - template_traverse_list, traverse_list = [ - self._resolved[traverse_list[0]] - ], traverse_list[1:] + template_traverse_list, traverse_list = ( + [self._resolved[traverse_list[0]]], + traverse_list[1:], + ) # Traverse a single symbol looking for any ReferenceTemplate objects while template_traverse_list: traverser, template_traverse_list = ( diff --git a/volatility3/framework/symbols/intermed.py b/volatility3/framework/symbols/intermed.py index 33035bc70..001f817bd 100644 --- a/volatility3/framework/symbols/intermed.py +++ b/volatility3/framework/symbols/intermed.py @@ -246,9 +246,12 @@ class IntermediateSymbolTable(interfaces.symbols.SymbolTableInterface): if name.endswith(zip_match + extension) or ( zip_match == "*" and name.endswith(extension) ): - yield "jar:file:" + str( - pathlib.Path(zip_path) - ) + "!" + name + yield ( + "jar:file:" + + str(pathlib.Path(zip_path)) + + "!" + + name + ) @classmethod def create( diff --git a/volatility3/framework/symbols/linux/extensions/__init__.py b/volatility3/framework/symbols/linux/extensions/__init__.py index 3b9a73e7c..a40c58bf4 100644 --- a/volatility3/framework/symbols/linux/extensions/__init__.py +++ b/volatility3/framework/symbols/linux/extensions/__init__.py @@ -36,7 +36,6 @@ vollog = logging.getLogger(__name__) class module(generic.GenericIntelProcess): - def is_valid(self): """Determine whether it is a valid module object by verifying the self-referential in module_kobject. This also confirms that the module is actively allocated and @@ -991,7 +990,6 @@ class maple_tree(objects.StructType): class mm_struct(objects.StructType): - # TODO: As of version 3.0.0 this method should be removed def get_mmap_iter(self) -> Iterable[interfaces.objects.ObjectInterface]: """ @@ -3048,7 +3046,6 @@ class latch_tree_root(objects.StructType): class kernel_symbol(objects.StructType): - def _offset_to_ptr(self, off) -> int: layer = self._context.layers[self.vol.layer_name] long_mask = (1 << layer.bits_per_register) - 1 diff --git a/volatility3/framework/symbols/linux/utilities/modules.py b/volatility3/framework/symbols/linux/utilities/modules.py index 0ae748814..804abd404 100644 --- a/volatility3/framework/symbols/linux/utilities/modules.py +++ b/volatility3/framework/symbols/linux/utilities/modules.py @@ -297,7 +297,6 @@ class Modules(interfaces.configuration.VersionableInterface): # process each module coming from back the current source for module in gatherer.gather_modules(context, kernel_module_name): - # the kernel sends back a ModuleInfo directly if isinstance(module, ModuleInfo): modinfo = module @@ -998,13 +997,16 @@ class ModuleDisplayPlugin(interfaces.configuration.VersionableInterface): with self.open(file_name) as file_handle: file_handle.write(elf_data) - yield 0, ( - format_hints.Hex(module.vol.offset), - name, - format_hints.Hex(code_size), - taints, - parameters, - file_name, + yield ( + 0, + ( + format_hints.Hex(module.vol.offset), + name, + format_hints.Hex(code_size), + taints, + parameters, + file_name, + ), ) def run(self): diff --git a/volatility3/framework/symbols/windows/extensions/callbacks.py b/volatility3/framework/symbols/windows/extensions/callbacks.py index f54db39f2..855933f59 100644 --- a/volatility3/framework/symbols/windows/extensions/callbacks.py +++ b/volatility3/framework/symbols/windows/extensions/callbacks.py @@ -48,7 +48,6 @@ class _SHUTDOWN_PACKET(objects.StructType, pool.ExecutiveObject): return False try: - device = self.DeviceObject if not device or not (device.DriverObject.DriverStart % 0x1000 == 0): vollog.debug( diff --git a/volatility3/framework/symbols/windows/extensions/gui.py b/volatility3/framework/symbols/windows/extensions/gui.py index 0d39f8173..000c50319 100644 --- a/volatility3/framework/symbols/windows/extensions/gui.py +++ b/volatility3/framework/symbols/windows/extensions/gui.py @@ -220,7 +220,6 @@ class GUIExtensions(interfaces.configuration.VersionableInterface): break class tagWND(objects.StructType, pool.ExecutiveObject): - def is_valid(self) -> bool: """ Enforce a valid sid diff --git a/volatility3/framework/symbols/windows/extensions/mft.py b/volatility3/framework/symbols/windows/extensions/mft.py index bf20c1ffc..ddc21798f 100644 --- a/volatility3/framework/symbols/windows/extensions/mft.py +++ b/volatility3/framework/symbols/windows/extensions/mft.py @@ -54,7 +54,6 @@ class MFTEntry(objects.StructType): return max(names, key=lambda x: len(str(x))) def _attributes(self) -> Iterator["MFTAttribute"]: - # We will update this on each pass in the next loop and use it as the new offset. attr_base_offset = self.FirstAttrOffset attribute_object_type_name = ( diff --git a/volatility3/framework/symbols/windows/extensions/shimcache.py b/volatility3/framework/symbols/windows/extensions/shimcache.py index 3b32d30c7..e7d92a48d 100644 --- a/volatility3/framework/symbols/windows/extensions/shimcache.py +++ b/volatility3/framework/symbols/windows/extensions/shimcache.py @@ -183,7 +183,6 @@ class SHIM_CACHE_ENTRY(objects.StructType): == self.ListEntry.Flink.Blink.dereference().vol.offset ) ): - return True else: return False diff --git a/volatility3/framework/symbols/windows/pdbconv.py b/volatility3/framework/symbols/windows/pdbconv.py index c23ffb350..5baf23fb2 100644 --- a/volatility3/framework/symbols/windows/pdbconv.py +++ b/volatility3/framework/symbols/windows/pdbconv.py @@ -489,7 +489,7 @@ class PdbReader: """Strips unnecessary components from the start of a symbol name.""" new_name = name - if new_name[:1] in ["_", "@", "\u007F"]: + if new_name[:1] in ["_", "@", "\u007f"]: new_name = new_name[1:] name_array = new_name.split("@") diff --git a/volatility3/plugins/__init__.py b/volatility3/plugins/__init__.py index 6afa8baf4..27fc0938c 100644 --- a/volatility3/plugins/__init__.py +++ b/volatility3/plugins/__init__.py @@ -12,6 +12,7 @@ are dependent upon, please DO NOT alter or remove this file unless you know the The framework is configured this way to allow plugin developers/users to override any plugin functionality whether existing or new. """ + from volatility3.framework import constants __path__ = constants.PLUGINS_PATH diff --git a/volatility3/plugins/linux/__init__.py b/volatility3/plugins/linux/__init__.py index 2d3e2386e..2ea8fb250 100644 --- a/volatility3/plugins/linux/__init__.py +++ b/volatility3/plugins/linux/__init__.py @@ -11,6 +11,7 @@ existing or new. When overriding the plugins directory, you must include a file like this in any subdirectories that may be necessary. """ + import os import sys diff --git a/volatility3/plugins/mac/__init__.py b/volatility3/plugins/mac/__init__.py index 3ac3f1553..3f8e81ce1 100644 --- a/volatility3/plugins/mac/__init__.py +++ b/volatility3/plugins/mac/__init__.py @@ -11,6 +11,7 @@ existing or new. When overriding the plugins directory, you must include a file like this in any subdirectories that may be necessary. """ + import os import sys diff --git a/volatility3/plugins/windows/__init__.py b/volatility3/plugins/windows/__init__.py index d74f4fcd5..468493508 100644 --- a/volatility3/plugins/windows/__init__.py +++ b/volatility3/plugins/windows/__init__.py @@ -11,6 +11,7 @@ existing or new. When overriding the plugins directory, you must include a file like this in any subdirectories that may be necessary. """ + import os import sys diff --git a/volatility3/plugins/windows/registry/__init__.py b/volatility3/plugins/windows/registry/__init__.py index aeeaa87f2..f012a52a0 100644 --- a/volatility3/plugins/windows/registry/__init__.py +++ b/volatility3/plugins/windows/registry/__init__.py @@ -11,6 +11,7 @@ existing or new. When overriding the plugins directory, you must include a file like this in any subdirectories that may be necessary. """ + import os import sys diff --git a/volatility3/symbols/__init__.py b/volatility3/symbols/__init__.py index c35f07cbe..162ea013e 100644 --- a/volatility3/symbols/__init__.py +++ b/volatility3/symbols/__init__.py @@ -6,6 +6,7 @@ This is the namespace for all volatility symbols, and determines the path for loading symbol ISF files """ + from volatility3.framework import constants __path__ = constants.SYMBOL_BASEPATHS From 7536e9dd14b7c80c3cdf868101af30c49d9a21c5 Mon Sep 17 00:00:00 2001 From: Mike Auty Date: Tue, 30 Sep 2025 19:07:59 +0100 Subject: [PATCH 71/86] Start to support uv installation of volatility --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 5837c3c9a..290a31abd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -54,7 +54,7 @@ test = [ docs = [ "volatility3[dev]", "sphinx>=4.0.0,<9", - "sphinx-autodoc-typehints>=3.0.0,<4", + "sphinx-autodoc-typehints>=3.0.0,<4; python_version >= '3.11'", "sphinx-rtd-theme>=3.0.1,<4", ] From 46c508fff2b3cc6f173645964124f2d9c807cd44 Mon Sep 17 00:00:00 2001 From: Mike Auty Date: Thu, 16 Oct 2025 20:29:02 +0100 Subject: [PATCH 72/86] Bump removal of plugins by a year to ensure suitable time for transition --- test/plugins/windows/test_scheduled_tasks.py | 2 +- test/plugins/windows/windows.py | 7 ++++--- volatility3/framework/plugins/windows/amcache.py | 5 +++-- volatility3/framework/plugins/windows/cachedump.py | 5 +++-- volatility3/framework/plugins/windows/hashdump.py | 5 +++-- volatility3/framework/plugins/windows/lsadump.py | 5 +++-- volatility3/framework/plugins/windows/scheduled_tasks.py | 5 +++-- 7 files changed, 20 insertions(+), 14 deletions(-) diff --git a/test/plugins/windows/test_scheduled_tasks.py b/test/plugins/windows/test_scheduled_tasks.py index 15d7f79a6..3369a5a35 100644 --- a/test/plugins/windows/test_scheduled_tasks.py +++ b/test/plugins/windows/test_scheduled_tasks.py @@ -4,7 +4,7 @@ import traceback import unittest sys.path.insert(0, "../../volatility3") -from volatility3.plugins.windows import scheduled_tasks +from volatility3.plugins.windows.registry import scheduled_tasks class TestActionsDecoding(unittest.TestCase): diff --git a/test/plugins/windows/windows.py b/test/plugins/windows/windows.py index 431461b6d..ff31ac109 100644 --- a/test/plugins/windows/windows.py +++ b/test/plugins/windows/windows.py @@ -4,6 +4,7 @@ import json import os import shutil import tempfile + from test import WindowsSamples, test_volatility @@ -437,7 +438,7 @@ class TestWindowsVadyarascan: class TestWindowsAmcache: def test_windows_generic_amcache(self, volatility, python, image): rc, out, _err = test_volatility.runvol_plugin( - "windows.amcache.Amcache", + "windows.registry.amcache.Amcache", image, volatility, python, @@ -492,7 +493,7 @@ class TestWindowsBigPools: # class TestWindowsCachedump: # def test_windows_generic_cachedump(self, volatility, python, image): # rc, out, _err = test_volatility.runvol_plugin( -# "windows.cachedump.Cachedump", +# "windows.registry.cachedump.Cachedump", # image, # volatility, # python, @@ -820,7 +821,7 @@ class TestWindowsLsadump: def test_windows_specific_lsadump(self, volatility, python): image = WindowsSamples.WINDOWSXP_GENERIC.value.path rc, out, _err = test_volatility.runvol_plugin( - "windows.lsadump.Lsadump", + "windows.registry.lsadump.Lsadump", image, volatility, python, diff --git a/volatility3/framework/plugins/windows/amcache.py b/volatility3/framework/plugins/windows/amcache.py index be144be91..0d1450127 100644 --- a/volatility3/framework/plugins/windows/amcache.py +++ b/volatility3/framework/plugins/windows/amcache.py @@ -2,7 +2,8 @@ # which is available at https://www.volatilityfoundation.org/license/vsl-v1.0 # import logging -from volatility3.framework import interfaces, deprecation + +from volatility3.framework import deprecation, interfaces from volatility3.plugins.windows.registry import amcache vollog = logging.getLogger(__name__) @@ -12,7 +13,7 @@ class Amcache( interfaces.plugins.PluginInterface, deprecation.PluginRenameClass, replacement_class=amcache.Amcache, - removal_date="2025-09-25", + removal_date="2026-09-25", ): """Extract information on executed applications from the AmCache (deprecated).""" diff --git a/volatility3/framework/plugins/windows/cachedump.py b/volatility3/framework/plugins/windows/cachedump.py index 35127c6f3..3c474bc2c 100644 --- a/volatility3/framework/plugins/windows/cachedump.py +++ b/volatility3/framework/plugins/windows/cachedump.py @@ -2,7 +2,8 @@ # which is available at https://www.volatilityfoundation.org/license/vsl-v1.0 # import logging -from volatility3.framework import interfaces, deprecation + +from volatility3.framework import deprecation, interfaces from volatility3.plugins.windows.registry import cachedump vollog = logging.getLogger(__name__) @@ -12,7 +13,7 @@ class Cachedump( interfaces.plugins.PluginInterface, deprecation.PluginRenameClass, replacement_class=cachedump.Cachedump, - removal_date="2025-09-25", + removal_date="2026-09-25", ): """Dumps lsa secrets from memory (deprecated)""" diff --git a/volatility3/framework/plugins/windows/hashdump.py b/volatility3/framework/plugins/windows/hashdump.py index e496e77a9..c4b99d86f 100644 --- a/volatility3/framework/plugins/windows/hashdump.py +++ b/volatility3/framework/plugins/windows/hashdump.py @@ -2,7 +2,8 @@ # which is available at https://www.volatilityfoundation.org/license/vsl-v1.0 # import logging -from volatility3.framework import interfaces, deprecation + +from volatility3.framework import deprecation, interfaces from volatility3.plugins.windows.registry import hashdump vollog = logging.getLogger(__name__) @@ -12,7 +13,7 @@ class Hashdump( interfaces.plugins.PluginInterface, deprecation.PluginRenameClass, replacement_class=hashdump.Hashdump, - removal_date="2025-09-25", + removal_date="2026-09-25", ): """Dumps user hashes from memory (deprecated)""" diff --git a/volatility3/framework/plugins/windows/lsadump.py b/volatility3/framework/plugins/windows/lsadump.py index 0b36ddef0..ab81f18df 100644 --- a/volatility3/framework/plugins/windows/lsadump.py +++ b/volatility3/framework/plugins/windows/lsadump.py @@ -2,7 +2,8 @@ # which is available at https://www.volatilityfoundation.org/license/vsl-v1.0 # import logging -from volatility3.framework import interfaces, deprecation + +from volatility3.framework import deprecation, interfaces from volatility3.plugins.windows.registry import lsadump vollog = logging.getLogger(__name__) @@ -12,7 +13,7 @@ class Lsadump( interfaces.plugins.PluginInterface, deprecation.PluginRenameClass, replacement_class=lsadump.Lsadump, - removal_date="2025-09-25", + removal_date="2026-09-25", ): """Dumps lsa secrets from memory (deprecated)""" diff --git a/volatility3/framework/plugins/windows/scheduled_tasks.py b/volatility3/framework/plugins/windows/scheduled_tasks.py index 62d8e3b88..104d062b2 100644 --- a/volatility3/framework/plugins/windows/scheduled_tasks.py +++ b/volatility3/framework/plugins/windows/scheduled_tasks.py @@ -2,7 +2,8 @@ # which is available at https://www.volatilityfoundation.org/license/vsl-v1.0 # import logging -from volatility3.framework import interfaces, deprecation + +from volatility3.framework import deprecation, interfaces from volatility3.plugins.windows.registry import scheduled_tasks vollog = logging.getLogger(__name__) @@ -12,7 +13,7 @@ class ScheduledTasks( interfaces.plugins.PluginInterface, deprecation.PluginRenameClass, replacement_class=scheduled_tasks.ScheduledTasks, - removal_date="2025-09-25", + removal_date="2026-09-25", ): """Decodes scheduled task information from the Windows registry, including information about triggers, actions, run times, and creation times (deprecated).""" From f13ad125d7e5d6e9e7d5b618a3a24ee8c5e5315b Mon Sep 17 00:00:00 2001 From: Mike Auty Date: Thu, 16 Oct 2025 20:33:28 +0100 Subject: [PATCH 73/86] Update the expiry dates of various functions to at least a year's notice --- volatility3/framework/plugins/linux/lsmod.py | 6 ++--- .../plugins/linux/malware/check_modules.py | 8 +++--- .../plugins/linux/malware/hidden_modules.py | 19 +++++++------- .../plugins/linux/malware/modxview.py | 11 ++++---- .../plugins/linux/malware/netfilter.py | 25 +++++++++++++------ 5 files changed, 39 insertions(+), 30 deletions(-) diff --git a/volatility3/framework/plugins/linux/lsmod.py b/volatility3/framework/plugins/linux/lsmod.py index 8ed52e3b7..ac6475264 100644 --- a/volatility3/framework/plugins/linux/lsmod.py +++ b/volatility3/framework/plugins/linux/lsmod.py @@ -4,10 +4,10 @@ """A module containing a plugin that lists loaded kernel modules.""" import logging -from typing import List, Iterable +from typing import Iterable, List import volatility3.framework.symbols.linux.utilities.modules as linux_utilities_modules -from volatility3.framework import interfaces, deprecation +from volatility3.framework import deprecation, interfaces from volatility3.framework.configuration import requirements from volatility3.framework.interfaces import plugins @@ -38,7 +38,7 @@ class Lsmod(plugins.PluginInterface): @deprecation.deprecated_method( replacement=linux_utilities_modules.Modules.list_modules, replacement_version=(3, 0, 0), - removal_date="2025-09-25", + removal_date="2026-03-25", ) def list_modules( cls, context: interfaces.context.ContextInterface, vmlinux_module_name: str diff --git a/volatility3/framework/plugins/linux/malware/check_modules.py b/volatility3/framework/plugins/linux/malware/check_modules.py index 7805bbd8a..65d358a45 100644 --- a/volatility3/framework/plugins/linux/malware/check_modules.py +++ b/volatility3/framework/plugins/linux/malware/check_modules.py @@ -3,14 +3,14 @@ # import logging -from typing import List, Dict, Generator +from typing import Dict, Generator, List import volatility3.framework.symbols.linux.utilities.modules as linux_utilities_modules -from volatility3.framework import interfaces, deprecation +from volatility3.framework import deprecation, interfaces from volatility3.framework.configuration import requirements +from volatility3.framework.interfaces import plugins from volatility3.framework.objects import utility from volatility3.framework.symbols.linux import extensions -from volatility3.framework.interfaces import plugins vollog = logging.getLogger(__name__) @@ -61,7 +61,7 @@ class Check_modules(plugins.PluginInterface): @classmethod @deprecation.deprecated_method( replacement=linux_utilities_modules.Modules.get_kset_modules, - removal_date="2025-09-25", + removal_date="2026-03-25", replacement_version=(3, 0, 0), ) def get_kset_modules( diff --git a/volatility3/framework/plugins/linux/malware/hidden_modules.py b/volatility3/framework/plugins/linux/malware/hidden_modules.py index dcd602c5d..f7944cea0 100644 --- a/volatility3/framework/plugins/linux/malware/hidden_modules.py +++ b/volatility3/framework/plugins/linux/malware/hidden_modules.py @@ -2,14 +2,15 @@ # which is available at https://www.volatilityfoundation.org/license/vsl-v1.0 # import logging -from typing import List, Set, Tuple, Iterable +from typing import Iterable, List, Set, Tuple + +from volatility3.framework import deprecation, exceptions, interfaces +from volatility3.framework.configuration import requirements +from volatility3.framework.interfaces import plugins +from volatility3.framework.symbols.linux import extensions from volatility3.framework.symbols.linux.utilities import ( modules as linux_utilities_modules, ) -from volatility3.framework import interfaces, exceptions, deprecation -from volatility3.framework.configuration import requirements -from volatility3.framework.symbols.linux import extensions -from volatility3.framework.interfaces import plugins vollog = logging.getLogger(__name__) @@ -103,7 +104,7 @@ class Hidden_modules(plugins.PluginInterface): @staticmethod @deprecation.deprecated_method( replacement=linux_utilities_modules.Modules.get_modules_memory_boundaries, - removal_date="2025-09-25", + removal_date="2026-03-25", replacement_version=(3, 0, 0), ) def get_modules_memory_boundaries( @@ -116,7 +117,7 @@ class Hidden_modules(plugins.PluginInterface): @deprecation.deprecated_method( replacement=linux_utilities_modules.Modules.get_module_address_alignment, - removal_date="2025-09-25", + removal_date="2026-03-25", replacement_version=(3, 0, 0), ) @classmethod @@ -144,13 +145,13 @@ class Hidden_modules(plugins.PluginInterface): @deprecation.deprecated_method( replacement=linux_utilities_modules.Modules.get_hidden_modules, - removal_date="2025-09-25", + removal_date="2026-03-25", replacement_version=(3, 0, 0), ) @staticmethod @deprecation.deprecated_method( replacement=linux_utilities_modules.Modules.validate_alignment_patterns, - removal_date="2025-09-25", + removal_date="2026-03-25", replacement_version=(3, 0, 0), ) def _validate_alignment_patterns( diff --git a/volatility3/framework/plugins/linux/malware/modxview.py b/volatility3/framework/plugins/linux/malware/modxview.py index c1707d26f..63b265202 100644 --- a/volatility3/framework/plugins/linux/malware/modxview.py +++ b/volatility3/framework/plugins/linux/malware/modxview.py @@ -2,15 +2,14 @@ # which is available at https://www.volatilityfoundation.org/license/vsl-v1.0 # import logging -from typing import List, Dict, Iterator +from typing import Dict, Iterator, List import volatility3.framework.symbols.linux.utilities.modules as linux_utilities_modules - -from volatility3.framework import interfaces, deprecation, renderers +from volatility3.framework import deprecation, interfaces, renderers from volatility3.framework.configuration import requirements +from volatility3.framework.constants import architectures 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 vollog = logging.getLogger(__name__) @@ -66,7 +65,7 @@ spot modules presence and taints.""" @deprecation.deprecated_method( replacement=linux_utilities_modules.Modules.flatten_run_modules_results, replacement_version=(3, 0, 0), - removal_date="2025-09-25", + removal_date="2026-03-25", ) def flatten_run_modules_results( cls, run_results: Dict[str, List[extensions.module]], deduplicate: bool = True @@ -89,7 +88,7 @@ spot modules presence and taints.""" @deprecation.deprecated_method( replacement=linux_utilities_modules.Modules.run_modules_scanners, replacement_version=(3, 0, 0), - removal_date="2025-09-25", + removal_date="2026-03-25", ) def run_modules_scanners( cls, diff --git a/volatility3/framework/plugins/linux/malware/netfilter.py b/volatility3/framework/plugins/linux/malware/netfilter.py index d724d4296..bd7f2b7cc 100644 --- a/volatility3/framework/plugins/linux/malware/netfilter.py +++ b/volatility3/framework/plugins/linux/malware/netfilter.py @@ -1,22 +1,22 @@ # This file is Copyright 2024 Volatility Foundation and licensed under the Volatility Software License 1.0 # which is available at https://www.volatilityfoundation.org/license/vsl-v1.0 # -from dataclasses import dataclass, field -from abc import ABC, abstractmethod import logging +from abc import ABC, abstractmethod +from dataclasses import dataclass, field +from typing import Iterator, List, Optional, Tuple import volatility3.framework.symbols.linux.utilities.modules as linux_utilities_modules -from typing import Iterator, List, Tuple, Optional from volatility3 import framework from volatility3.framework import ( constants, + deprecation, + exceptions, interfaces, renderers, - exceptions, - deprecation, ) -from volatility3.framework.renderers import format_hints from volatility3.framework.configuration import requirements +from volatility3.framework.renderers import format_hints from volatility3.framework.symbols.linux import network vollog = logging.getLogger(__name__) @@ -223,7 +223,16 @@ class AbstractNetfilter(ABC): ) hooked = module_info is None - yield netns, proto_name, hook_name, priority, hook_ops_hook, module_info, symbol_name, hooked + yield ( + netns, + proto_name, + hook_name, + priority, + hook_ops_hook, + module_info, + symbol_name, + hooked, + ) @classmethod @abstractmethod @@ -304,7 +313,7 @@ class AbstractNetfilter(ABC): return ("IPV4", "ARP", "BRIDGE", "IPV6", "DECNET") @deprecation.method_being_removed( - removal_date="2025-09-25", + removal_date="2026-03-25", message="Callers to this method should adapt `linux_utilities_modules.Modules.run_module_scanners`", ) def get_module_name_for_address(self, addr) -> str: From 5ef5c027ec0a0cc9ff81798e64e8644fb370bcc5 Mon Sep 17 00:00:00 2001 From: Abyss Watcher Date: Mon, 10 Nov 2025 22:10:24 +0100 Subject: [PATCH 74/86] fix missing kernel_base increment in low stub scanner --- volatility3/framework/automagic/pdbscan.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/volatility3/framework/automagic/pdbscan.py b/volatility3/framework/automagic/pdbscan.py index 8e2233840..c03db7582 100644 --- a/volatility3/framework/automagic/pdbscan.py +++ b/volatility3/framework/automagic/pdbscan.py @@ -450,7 +450,7 @@ class KernelPDBScanner(interfaces.automagic.AutomagicInterface): while (kernel_base + 0x2000000) > kernel_hint: for i in range(0, 0x200000, 0x1000): valid_kernel = self.check_kernel_offset( - context, vlayer, kernel_base, progress_callback + context, vlayer, kernel_base + i, progress_callback ) if valid_kernel: return valid_kernel From 81f135f315d2beec9f1ab5cedad0fc26df40700f Mon Sep 17 00:00:00 2001 From: Mike Auty Date: Thu, 8 May 2025 14:29:01 +0100 Subject: [PATCH 75/86] Linux: Rework module calls to parameterize rather than pull in methods --- volatility3/framework/plugins/linux/lsmod.py | 45 +++++++++--- .../plugins/linux/malware/check_modules.py | 43 +++++++++--- .../plugins/linux/malware/hidden_modules.py | 61 ++++++++++++----- .../symbols/linux/utilities/modules.py | 68 ++++++++----------- 4 files changed, 145 insertions(+), 72 deletions(-) diff --git a/volatility3/framework/plugins/linux/lsmod.py b/volatility3/framework/plugins/linux/lsmod.py index ac6475264..8db47cb95 100644 --- a/volatility3/framework/plugins/linux/lsmod.py +++ b/volatility3/framework/plugins/linux/lsmod.py @@ -4,10 +4,10 @@ """A module containing a plugin that lists loaded kernel modules.""" import logging -from typing import Iterable, List +from typing import List, Iterable import volatility3.framework.symbols.linux.utilities.modules as linux_utilities_modules -from volatility3.framework import deprecation, interfaces +from volatility3.framework import constants, interfaces, deprecation, renderers from volatility3.framework.configuration import requirements from volatility3.framework.interfaces import plugins @@ -18,27 +18,41 @@ class Lsmod(plugins.PluginInterface): """Lists loaded kernel modules.""" _required_framework_version = (2, 0, 0) - _version = (3, 0, 1) + _version = (3, 0, 3) - run = linux_utilities_modules.ModuleDisplayPlugin.run - _generator = linux_utilities_modules.ModuleDisplayPlugin.generator implementation = linux_utilities_modules.Modules.list_modules @classmethod def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]: return [ + requirements.ModuleRequirement( + name="kernel", + description="Linux kernel", + architectures=constants.architectures.LINUX_ARCHS, + ), + requirements.VersionRequirement( + name="linux_utilities_modules", + component=linux_utilities_modules.Modules, + version=(3, 0, 0), + ), requirements.VersionRequirement( name="linux_utilities_modules_module_display_plugin", component=linux_utilities_modules.ModuleDisplayPlugin, - version=(1, 0, 0), + version=(2, 0, 0), ), - ] + linux_utilities_modules.ModuleDisplayPlugin.get_requirements() + requirements.BooleanRequirement( + name="dump", + description="Extract listed modules", + default=False, + optional=True, + ), + ] @classmethod @deprecation.deprecated_method( replacement=linux_utilities_modules.Modules.list_modules, replacement_version=(3, 0, 0), - removal_date="2026-03-25", + removal_date="2025-09-25", ) def list_modules( cls, context: interfaces.context.ContextInterface, vmlinux_module_name: str @@ -46,3 +60,18 @@ class Lsmod(plugins.PluginInterface): return linux_utilities_modules.Modules.list_modules( context, vmlinux_module_name ) + + def run(self): + return renderers.TreeGrid( + linux_utilities_modules.ModuleDisplayPlugin.columns_results, + self._generator(), + ) + + def _generator(self): + yield from linux_utilities_modules.ModuleDisplayPlugin.generate_results( + self.context, + self.implementation, + self.config["kernel"], + self.config["dump"], + self.open, + ) diff --git a/volatility3/framework/plugins/linux/malware/check_modules.py b/volatility3/framework/plugins/linux/malware/check_modules.py index 65d358a45..0844b3400 100644 --- a/volatility3/framework/plugins/linux/malware/check_modules.py +++ b/volatility3/framework/plugins/linux/malware/check_modules.py @@ -3,19 +3,18 @@ # import logging -from typing import Dict, Generator, List +from typing import List, Dict, Generator import volatility3.framework.symbols.linux.utilities.modules as linux_utilities_modules -from volatility3.framework import deprecation, interfaces +from volatility3.framework import constants, interfaces, deprecation, renderers from volatility3.framework.configuration import requirements -from volatility3.framework.interfaces import plugins from volatility3.framework.objects import utility from volatility3.framework.symbols.linux import extensions vollog = logging.getLogger(__name__) -class Check_modules(plugins.PluginInterface): +class Check_modules(interfaces.plugins.PluginInterface): """Compares module list to sysfs info, if available""" _version = (3, 0, 1) @@ -23,7 +22,7 @@ class Check_modules(plugins.PluginInterface): @classmethod def compare_kset_and_lsmod( - cls, context: str, vmlinux_name: str + cls, context: interfaces.context.ContextInterface, vmlinux_name: str ) -> Generator[extensions.module, None, None]: kset_modules = linux_utilities_modules.Modules.get_kset_modules( context=context, vmlinux_name=vmlinux_name @@ -39,13 +38,16 @@ class Check_modules(plugins.PluginInterface): for mod_name in set(kset_modules.keys()).difference(lsmod_modules): yield kset_modules[mod_name] - run = linux_utilities_modules.ModuleDisplayPlugin.run - _generator = linux_utilities_modules.ModuleDisplayPlugin.generator implementation = compare_kset_and_lsmod @classmethod def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]: return [ + requirements.ModuleRequirement( + name="kernel", + description="Linux kernel", + architectures=constants.architectures.LINUX_ARCHS, + ), requirements.VersionRequirement( name="modules", component=linux_utilities_modules.Modules, @@ -54,17 +56,38 @@ class Check_modules(plugins.PluginInterface): requirements.VersionRequirement( name="linux_utilities_modules_module_display_plugin", component=linux_utilities_modules.ModuleDisplayPlugin, - version=(1, 0, 0), + version=(2, 0, 0), ), - ] + linux_utilities_modules.ModuleDisplayPlugin.get_requirements() + requirements.BooleanRequirement( + name="dump", + description="Extract listed modules", + default=False, + optional=True, + ), + ] @classmethod @deprecation.deprecated_method( replacement=linux_utilities_modules.Modules.get_kset_modules, - removal_date="2026-03-25", + removal_date="2025-09-25", replacement_version=(3, 0, 0), ) def get_kset_modules( cls, context: interfaces.context.ContextInterface, vmlinux_name: str ) -> Dict[str, extensions.module]: return linux_utilities_modules.Modules.get_kset_modules(context, vmlinux_name) + + def run(self): + return renderers.TreeGrid( + linux_utilities_modules.ModuleDisplayPlugin.columns_results, + self._generator(), + ) + + def _generator(self): + yield from linux_utilities_modules.ModuleDisplayPlugin.generate_results( + self.context, + self.implementation, + self.config["kernel"], + self.config["dump"], + self.open, + ) diff --git a/volatility3/framework/plugins/linux/malware/hidden_modules.py b/volatility3/framework/plugins/linux/malware/hidden_modules.py index f7944cea0..66f27ff66 100644 --- a/volatility3/framework/plugins/linux/malware/hidden_modules.py +++ b/volatility3/framework/plugins/linux/malware/hidden_modules.py @@ -2,15 +2,20 @@ # which is available at https://www.volatilityfoundation.org/license/vsl-v1.0 # import logging -from typing import Iterable, List, Set, Tuple - -from volatility3.framework import deprecation, exceptions, interfaces -from volatility3.framework.configuration import requirements -from volatility3.framework.interfaces import plugins -from volatility3.framework.symbols.linux import extensions +from typing import List, Set, Tuple, Iterable, Generator from volatility3.framework.symbols.linux.utilities import ( modules as linux_utilities_modules, ) +from volatility3.framework import ( + constants, + interfaces, + exceptions, + deprecation, + renderers, +) +from volatility3.framework.configuration import requirements +from volatility3.framework.symbols.linux import extensions +from volatility3.framework.interfaces import plugins vollog = logging.getLogger(__name__) @@ -19,12 +24,12 @@ class Hidden_modules(plugins.PluginInterface): """Carves memory to find hidden kernel modules""" _required_framework_version = (2, 25, 0) - _version = (3, 0, 2) + _version = (3, 0, 3) @classmethod def find_hidden_modules( cls, context, vmlinux_module_name: str - ) -> extensions.module: + ) -> Generator[extensions.module, None, None]: if context.symbol_space.verify_table_versions( "dwarf2json", lambda version, _: (not version) or version < (0, 8, 0) ): @@ -82,29 +87,38 @@ class Hidden_modules(plugins.PluginInterface): vmlinux_module_name, known_module_addresses, modules_memory_boundaries ) - run = linux_utilities_modules.ModuleDisplayPlugin.run - _generator = linux_utilities_modules.ModuleDisplayPlugin.generator implementation = find_hidden_modules @classmethod def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]: return [ + requirements.ModuleRequirement( + name="kernel", + description="Linux kernel", + architectures=constants.architectures.LINUX_ARCHS, + ), requirements.VersionRequirement( name="linux_utilities_modules_module_display_plugin", component=linux_utilities_modules.ModuleDisplayPlugin, - version=(1, 0, 0), + version=(2, 0, 0), ), requirements.VersionRequirement( name="linux_utilities_modules", component=linux_utilities_modules.Modules, version=(3, 0, 1), ), - ] + linux_utilities_modules.ModuleDisplayPlugin.get_requirements() + requirements.BooleanRequirement( + name="dump", + description="Extract listed modules", + default=False, + optional=True, + ), + ] @staticmethod @deprecation.deprecated_method( replacement=linux_utilities_modules.Modules.get_modules_memory_boundaries, - removal_date="2026-03-25", + removal_date="2025-09-25", replacement_version=(3, 0, 0), ) def get_modules_memory_boundaries( @@ -117,7 +131,7 @@ class Hidden_modules(plugins.PluginInterface): @deprecation.deprecated_method( replacement=linux_utilities_modules.Modules.get_module_address_alignment, - removal_date="2026-03-25", + removal_date="2025-09-25", replacement_version=(3, 0, 0), ) @classmethod @@ -145,13 +159,13 @@ class Hidden_modules(plugins.PluginInterface): @deprecation.deprecated_method( replacement=linux_utilities_modules.Modules.get_hidden_modules, - removal_date="2026-03-25", + removal_date="2025-09-25", replacement_version=(3, 0, 0), ) @staticmethod @deprecation.deprecated_method( replacement=linux_utilities_modules.Modules.validate_alignment_patterns, - removal_date="2026-03-25", + removal_date="2025-09-25", replacement_version=(3, 0, 0), ) def _validate_alignment_patterns( @@ -196,3 +210,18 @@ class Hidden_modules(plugins.PluginInterface): ) } return known_module_addresses + + def run(self): + return renderers.TreeGrid( + linux_utilities_modules.ModuleDisplayPlugin.columns_results, + self._generator(), + ) + + def _generator(self): + yield from linux_utilities_modules.ModuleDisplayPlugin.generate_results( + self.context, + self.implementation, + self.config["kernel"], + self.config["dump"], + self.open, + ) diff --git a/volatility3/framework/symbols/linux/utilities/modules.py b/volatility3/framework/symbols/linux/utilities/modules.py index 804abd404..3bc348c71 100644 --- a/volatility3/framework/symbols/linux/utilities/modules.py +++ b/volatility3/framework/symbols/linux/utilities/modules.py @@ -1,6 +1,7 @@ import logging import warnings from typing import ( + Callable, Iterable, Iterator, List, @@ -23,7 +24,6 @@ from volatility3.framework import ( objects, renderers, ) -from volatility3.framework.constants import architectures from volatility3.framework.renderers import format_hints from volatility3.framework.configuration import requirements from volatility3.framework.objects import utility @@ -382,7 +382,7 @@ class Modules(interfaces.configuration.VersionableInterface): vmlinux_module_name: str, known_module_addresses: Set[int], modules_memory_boundaries: Tuple, - ) -> Iterable[interfaces.objects.ObjectInterface]: + ) -> Iterable[extensions.module]: """Enumerate hidden modules by taking advantage of memory address alignment patterns This technique is much faster and uses less memory than the traditional scan method @@ -919,19 +919,11 @@ class ModuleDisplayPlugin(interfaces.configuration.VersionableInterface): The constructor of the plugin must call super() with the `implementation` set """ - _version = (1, 0, 1) - _required_framework_version = (2, 0, 0) - - framework.require_interface_version(*_required_framework_version) + _version = (2, 0, 0) @classmethod def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]: return [ - requirements.ModuleRequirement( - name="kernel", - description="Linux kernel", - architectures=architectures.LINUX_ARCHS, - ), requirements.VersionRequirement( name="linux_utilities_modules", component=Modules, @@ -940,25 +932,29 @@ class ModuleDisplayPlugin(interfaces.configuration.VersionableInterface): requirements.VersionRequirement( name="linux-tainting", component=tainting.Tainting, version=(1, 0, 0) ), - requirements.BooleanRequirement( - name="dump", - description="Extract listed modules", - default=False, - optional=True, - ), ] - def generator(self): + @classmethod + def generate_results( + cls, + context: interfaces.context.ContextInterface, + implementation: Callable[ + [interfaces.context.ContextInterface, str], Iterable[extensions.module] + ], + kernel_module_name: str, + dump: bool, + open_implementation: Optional[interfaces.plugins.FileHandlerInterface], + ): """ Uses the implementation set in the constructor call to produce consistent output fields across module gathering plugins """ - for module in self.implementation(self.context, self.config["kernel"]): + for module in implementation(context, kernel_module_name): try: name = utility.array_to_string(module.name) except exceptions.InvalidAddressException: vollog.debug( - f"Unable to recover name for module {module.vol.offset:#x} from implementation {self.implementation}" + f"Unable to recover name for module {module.vol.offset:#x} from implementation {implementation}" ) continue @@ -968,21 +964,21 @@ class ModuleDisplayPlugin(interfaces.configuration.VersionableInterface): taints = ",".join( tainting.Tainting.get_taints_parsed( - self.context, self.config["kernel"], module.taints, True + context, kernel_module_name, module.taints, True ) ) parameters_iter = Modules.get_load_parameters( - self.context, self.config["kernel"], module + context, kernel_module_name, module ) parameters = ", ".join([f"{key}={value}" for key, value in parameters_iter]) file_name = renderers.NotApplicableValue() - if self.config["dump"]: + if open_implementation: elf_data = linux_utilities_module_extract.ModuleExtract.extract_module( - self.context, self.config["kernel"], module + context, kernel_module_name, module ) if not elf_data: vollog.warning( @@ -990,11 +986,11 @@ class ModuleDisplayPlugin(interfaces.configuration.VersionableInterface): ) file_name = renderers.NotAvailableValue() else: - file_name = self.open.sanitize_filename( + file_name = open_implementation.sanitize_filename( f"kernel_module.{name}.{module.vol.offset:#x}.elf" ) - with self.open(file_name) as file_handle: + with open_implementation(file_name) as file_handle: file_handle.write(elf_data) yield ( @@ -1009,15 +1005,11 @@ class ModuleDisplayPlugin(interfaces.configuration.VersionableInterface): ), ) - def run(self): - return renderers.TreeGrid( - [ - ("Offset", format_hints.Hex), - ("Module Name", str), - ("Code Size", format_hints.Hex), - ("Taints", str), - ("Load Arguments", str), - ("File Output", str), - ], - self._generator(), - ) + columns_results = [ + ("Offset", format_hints.Hex), + ("Module Name", str), + ("Code Size", format_hints.Hex), + ("Taints", str), + ("Load Arguments", str), + ("File Output", str), + ] From 5c9c763f64a6e6be3946e70e03a19e44b4fc0f82 Mon Sep 17 00:00:00 2001 From: ikelos Date: Wed, 19 Nov 2025 16:34:53 +0000 Subject: [PATCH 76/86] Update volatility3/framework/plugins/linux/lsmod.py --- volatility3/framework/plugins/linux/lsmod.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/volatility3/framework/plugins/linux/lsmod.py b/volatility3/framework/plugins/linux/lsmod.py index 8db47cb95..3d4c04b26 100644 --- a/volatility3/framework/plugins/linux/lsmod.py +++ b/volatility3/framework/plugins/linux/lsmod.py @@ -52,7 +52,7 @@ class Lsmod(plugins.PluginInterface): @deprecation.deprecated_method( replacement=linux_utilities_modules.Modules.list_modules, replacement_version=(3, 0, 0), - removal_date="2025-09-25", + removal_date="2026-03-25", ) def list_modules( cls, context: interfaces.context.ContextInterface, vmlinux_module_name: str From 9454fe1f270ec1bd81b6af56e9e9488ad47d121f Mon Sep 17 00:00:00 2001 From: ikelos Date: Wed, 19 Nov 2025 16:35:01 +0000 Subject: [PATCH 77/86] Update volatility3/framework/symbols/linux/utilities/modules.py --- volatility3/framework/symbols/linux/utilities/modules.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/volatility3/framework/symbols/linux/utilities/modules.py b/volatility3/framework/symbols/linux/utilities/modules.py index 3bc348c71..0c5d6b38a 100644 --- a/volatility3/framework/symbols/linux/utilities/modules.py +++ b/volatility3/framework/symbols/linux/utilities/modules.py @@ -976,7 +976,7 @@ class ModuleDisplayPlugin(interfaces.configuration.VersionableInterface): file_name = renderers.NotApplicableValue() - if open_implementation: + if dump and open_implementation: elf_data = linux_utilities_module_extract.ModuleExtract.extract_module( context, kernel_module_name, module ) From 393abb2a2fcbddfade9cd692535460ad4087706b Mon Sep 17 00:00:00 2001 From: ikelos Date: Wed, 19 Nov 2025 16:35:08 +0000 Subject: [PATCH 78/86] Update volatility3/framework/plugins/linux/malware/hidden_modules.py --- volatility3/framework/plugins/linux/malware/hidden_modules.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/volatility3/framework/plugins/linux/malware/hidden_modules.py b/volatility3/framework/plugins/linux/malware/hidden_modules.py index 66f27ff66..043a9ac26 100644 --- a/volatility3/framework/plugins/linux/malware/hidden_modules.py +++ b/volatility3/framework/plugins/linux/malware/hidden_modules.py @@ -118,7 +118,7 @@ class Hidden_modules(plugins.PluginInterface): @staticmethod @deprecation.deprecated_method( replacement=linux_utilities_modules.Modules.get_modules_memory_boundaries, - removal_date="2025-09-25", + removal_date="2026-03-25", replacement_version=(3, 0, 0), ) def get_modules_memory_boundaries( From dfd6282f1738db850d0387f4e79b0f368cda7062 Mon Sep 17 00:00:00 2001 From: ikelos Date: Wed, 19 Nov 2025 16:35:15 +0000 Subject: [PATCH 79/86] Update volatility3/framework/plugins/linux/malware/hidden_modules.py --- volatility3/framework/plugins/linux/malware/hidden_modules.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/volatility3/framework/plugins/linux/malware/hidden_modules.py b/volatility3/framework/plugins/linux/malware/hidden_modules.py index 043a9ac26..bf9c98b97 100644 --- a/volatility3/framework/plugins/linux/malware/hidden_modules.py +++ b/volatility3/framework/plugins/linux/malware/hidden_modules.py @@ -131,7 +131,7 @@ class Hidden_modules(plugins.PluginInterface): @deprecation.deprecated_method( replacement=linux_utilities_modules.Modules.get_module_address_alignment, - removal_date="2025-09-25", + removal_date="2026-03-25", replacement_version=(3, 0, 0), ) @classmethod From 9bfb1bed3520f26726bf3ee83506585f78b79677 Mon Sep 17 00:00:00 2001 From: ikelos Date: Wed, 19 Nov 2025 16:35:22 +0000 Subject: [PATCH 80/86] Update volatility3/framework/plugins/linux/malware/hidden_modules.py --- volatility3/framework/plugins/linux/malware/hidden_modules.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/volatility3/framework/plugins/linux/malware/hidden_modules.py b/volatility3/framework/plugins/linux/malware/hidden_modules.py index bf9c98b97..ece96101e 100644 --- a/volatility3/framework/plugins/linux/malware/hidden_modules.py +++ b/volatility3/framework/plugins/linux/malware/hidden_modules.py @@ -159,7 +159,7 @@ class Hidden_modules(plugins.PluginInterface): @deprecation.deprecated_method( replacement=linux_utilities_modules.Modules.get_hidden_modules, - removal_date="2025-09-25", + removal_date="2026-03-25", replacement_version=(3, 0, 0), ) @staticmethod From 83bf743a27e1e7faf0b9027e768a204e874fd7c1 Mon Sep 17 00:00:00 2001 From: ikelos Date: Wed, 19 Nov 2025 16:35:28 +0000 Subject: [PATCH 81/86] Update volatility3/framework/plugins/linux/malware/hidden_modules.py --- volatility3/framework/plugins/linux/malware/hidden_modules.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/volatility3/framework/plugins/linux/malware/hidden_modules.py b/volatility3/framework/plugins/linux/malware/hidden_modules.py index ece96101e..22e089c10 100644 --- a/volatility3/framework/plugins/linux/malware/hidden_modules.py +++ b/volatility3/framework/plugins/linux/malware/hidden_modules.py @@ -165,7 +165,7 @@ class Hidden_modules(plugins.PluginInterface): @staticmethod @deprecation.deprecated_method( replacement=linux_utilities_modules.Modules.validate_alignment_patterns, - removal_date="2025-09-25", + removal_date="2026-03-25", replacement_version=(3, 0, 0), ) def _validate_alignment_patterns( From c54daae32a53c6dc987770eb15bb07ddddac3152 Mon Sep 17 00:00:00 2001 From: ikelos Date: Wed, 19 Nov 2025 16:35:35 +0000 Subject: [PATCH 82/86] Update volatility3/framework/plugins/linux/malware/check_modules.py --- volatility3/framework/plugins/linux/malware/check_modules.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/volatility3/framework/plugins/linux/malware/check_modules.py b/volatility3/framework/plugins/linux/malware/check_modules.py index 0844b3400..bc7d270aa 100644 --- a/volatility3/framework/plugins/linux/malware/check_modules.py +++ b/volatility3/framework/plugins/linux/malware/check_modules.py @@ -69,7 +69,7 @@ class Check_modules(interfaces.plugins.PluginInterface): @classmethod @deprecation.deprecated_method( replacement=linux_utilities_modules.Modules.get_kset_modules, - removal_date="2025-09-25", + removal_date="2026-03-25", replacement_version=(3, 0, 0), ) def get_kset_modules( From ea4aab5652f552e5131806d2ffd369e470171884 Mon Sep 17 00:00:00 2001 From: Mike Auty Date: Wed, 19 Nov 2025 16:39:02 +0000 Subject: [PATCH 83/86] Fix black/ruff checks --- volatility3/framework/plugins/linux/lsmod.py | 4 ++-- .../plugins/linux/malware/check_modules.py | 4 ++-- .../plugins/linux/malware/hidden_modules.py | 15 ++++++++------- 3 files changed, 12 insertions(+), 11 deletions(-) diff --git a/volatility3/framework/plugins/linux/lsmod.py b/volatility3/framework/plugins/linux/lsmod.py index 3d4c04b26..e0878647f 100644 --- a/volatility3/framework/plugins/linux/lsmod.py +++ b/volatility3/framework/plugins/linux/lsmod.py @@ -4,10 +4,10 @@ """A module containing a plugin that lists loaded kernel modules.""" import logging -from typing import List, Iterable +from typing import Iterable, List import volatility3.framework.symbols.linux.utilities.modules as linux_utilities_modules -from volatility3.framework import constants, interfaces, deprecation, renderers +from volatility3.framework import constants, deprecation, interfaces, renderers from volatility3.framework.configuration import requirements from volatility3.framework.interfaces import plugins diff --git a/volatility3/framework/plugins/linux/malware/check_modules.py b/volatility3/framework/plugins/linux/malware/check_modules.py index bc7d270aa..1834a616e 100644 --- a/volatility3/framework/plugins/linux/malware/check_modules.py +++ b/volatility3/framework/plugins/linux/malware/check_modules.py @@ -3,10 +3,10 @@ # import logging -from typing import List, Dict, Generator +from typing import Dict, Generator, List import volatility3.framework.symbols.linux.utilities.modules as linux_utilities_modules -from volatility3.framework import constants, interfaces, deprecation, renderers +from volatility3.framework import constants, deprecation, interfaces, renderers from volatility3.framework.configuration import requirements from volatility3.framework.objects import utility from volatility3.framework.symbols.linux import extensions diff --git a/volatility3/framework/plugins/linux/malware/hidden_modules.py b/volatility3/framework/plugins/linux/malware/hidden_modules.py index 22e089c10..a9f0b5b51 100644 --- a/volatility3/framework/plugins/linux/malware/hidden_modules.py +++ b/volatility3/framework/plugins/linux/malware/hidden_modules.py @@ -2,20 +2,21 @@ # which is available at https://www.volatilityfoundation.org/license/vsl-v1.0 # import logging -from typing import List, Set, Tuple, Iterable, Generator -from volatility3.framework.symbols.linux.utilities import ( - modules as linux_utilities_modules, -) +from typing import Generator, Iterable, List, Set, Tuple + from volatility3.framework import ( constants, - interfaces, - exceptions, deprecation, + exceptions, + interfaces, renderers, ) from volatility3.framework.configuration import requirements -from volatility3.framework.symbols.linux import extensions from volatility3.framework.interfaces import plugins +from volatility3.framework.symbols.linux import extensions +from volatility3.framework.symbols.linux.utilities import ( + modules as linux_utilities_modules, +) vollog = logging.getLogger(__name__) From 4ce58a636dd3facabb11b17a13c744c9ac15fe3e Mon Sep 17 00:00:00 2001 From: Mike Auty Date: Wed, 19 Nov 2025 16:49:50 +0000 Subject: [PATCH 84/86] Reorder imports and fix black issue --- .../symbols/linux/utilities/modules.py | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/volatility3/framework/symbols/linux/utilities/modules.py b/volatility3/framework/symbols/linux/utilities/modules.py index 0c5d6b38a..2b16ec6e0 100644 --- a/volatility3/framework/symbols/linux/utilities/modules.py +++ b/volatility3/framework/symbols/linux/utilities/modules.py @@ -1,37 +1,36 @@ import logging import warnings +from abc import ABCMeta, abstractmethod from typing import ( Callable, + Dict, + Generator, Iterable, Iterator, List, - Optional, - Tuple, NamedTuple, - Dict, + Optional, Set, - Generator, + Tuple, Union, ) -from abc import ABCMeta, abstractmethod +import volatility3.framework.symbols.linux.utilities.module_extract as linux_utilities_module_extract from volatility3 import framework from volatility3.framework import ( constants, - interfaces, deprecation, exceptions, + interfaces, objects, renderers, ) -from volatility3.framework.renderers import format_hints from volatility3.framework.configuration import requirements from volatility3.framework.objects import utility +from volatility3.framework.renderers import format_hints from volatility3.framework.symbols.linux import extensions from volatility3.framework.symbols.linux.utilities import tainting -import volatility3.framework.symbols.linux.utilities.module_extract as linux_utilities_module_extract - vollog = logging.getLogger(__name__) @@ -976,7 +975,7 @@ class ModuleDisplayPlugin(interfaces.configuration.VersionableInterface): file_name = renderers.NotApplicableValue() - if dump and open_implementation: + if dump and open_implementation: elf_data = linux_utilities_module_extract.ModuleExtract.extract_module( context, kernel_module_name, module ) From 6b9a6f84c342608cc8817e33556629fb5a4c7110 Mon Sep 17 00:00:00 2001 From: Mike Auty Date: Thu, 29 Jan 2026 20:39:09 +0000 Subject: [PATCH 85/86] Bump year for copyrights to 2026 --- README.md | 2 +- doc/source/conf.py | 7 +++---- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 79401a18f..8c5332a90 100644 --- a/README.md +++ b/README.md @@ -96,7 +96,7 @@ The latest generated copy of the documentation can be found at: Date: Thu, 29 Jan 2026 21:04:51 +0000 Subject: [PATCH 86/86] Add arrow support to pyinstaller build --- .github/workflows/build-pyinstaller.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build-pyinstaller.yml b/.github/workflows/build-pyinstaller.yml index 7d9d86fb0..a667eed03 100644 --- a/.github/workflows/build-pyinstaller.yml +++ b/.github/workflows/build-pyinstaller.yml @@ -28,7 +28,7 @@ jobs: run: | python -m pip install --upgrade pip pip install pyinstaller - pip install -e .[full,cloud] + pip install -e .[full,cloud,arrow] - name: Pyinstall executable run: |