Merge pull request #1766 from volatilityfoundation/thrdscan/filtering_and_tracebacks

Windows ThrdScan: Thread filtering, type-hints and tracebacks
This commit is contained in:
ikelos
2025-04-13 15:55:43 +01:00
committed by GitHub
4 changed files with 71 additions and 61 deletions
+1 -1
View File
@@ -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 = 1 # Number of changes that do not change the interface
VERSION_PATCH = 2 # Number of changes that do not change the interface
VERSION_SUFFIX = ""
PACKAGE_VERSION = (
@@ -28,3 +28,5 @@ PROCESSOR_START_BLOCK_LM_TARGET_OFFSET = (
# CR3 register within structures describing initial processor state to be started
PROCESSOR_START_BLOCK_CR3_OFFSET = 0xA0 # PROCESSOR_START_BLOCK->ProcessorState->SpecialRegisters->Cr3, ULONG64 8 bytes
MAX_PID = 0xFFFFFFFC
@@ -1,15 +1,17 @@
##
## plugin for testing addition of threads scan support to poolscanner.py
##
import logging
import datetime
from typing import Callable, Iterable, Tuple, Optional, Dict
import logging
from typing import Callable, Dict, NamedTuple, Optional, Union, Tuple, Iterator
from volatility3.framework import renderers, interfaces, exceptions
from volatility3.framework import exceptions, interfaces, objects, renderers
from volatility3.framework.configuration import requirements
from volatility3.framework.constants import windows as windows_constants
from volatility3.framework.renderers import format_hints
from volatility3.plugins.windows import poolscanner, pe_symbols
from volatility3.framework.symbols.windows import extensions as win_extensions
from volatility3.plugins import timeliner
from volatility3.plugins.windows import pe_symbols, poolscanner
vollog = logging.getLogger(__name__)
@@ -19,7 +21,18 @@ class ThrdScan(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface)
# version 2.6.0 adds support for scanning for 'Ethread' structures by pool tags
_required_framework_version = (2, 6, 0)
_version = (2, 0, 0)
_version = (2, 1, 0)
class ThreadInfo(NamedTuple):
offset: int
pid: objects.Pointer
tid: objects.Pointer
start_addr: objects.Pointer
start_path: Optional[str]
win32_start_addr: objects.Pointer
win32_start_path: Optional[str]
create_time: Union[datetime.datetime, interfaces.renderers.BaseAbsentValue]
exit_time: Union[datetime.datetime, interfaces.renderers.BaseAbsentValue]
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
@@ -51,7 +64,7 @@ class ThrdScan(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface)
cls,
context: interfaces.context.ContextInterface,
module_name: str,
) -> Iterable[interfaces.objects.ObjectInterface]:
) -> Iterator[win_extensions.ETHREAD]:
"""Scans for threads using the poolscanner module and constraints.
Args:
@@ -77,19 +90,9 @@ class ThrdScan(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface)
@classmethod
def gather_thread_info(
cls,
ethread: interfaces.objects.ObjectInterface,
vads_cache: Dict[int, pe_symbols.ranges_type] = None,
) -> Tuple[
int,
int,
int,
int,
Optional[str],
int,
Optional[str],
Optional[datetime.datetime],
Optional[datetime.datetime],
]:
ethread: win_extensions.ETHREAD,
vads_cache: Optional[Dict[int, pe_symbols.ranges_type]] = None,
) -> Optional[ThreadInfo]:
try:
thread_offset = ethread.vol.offset
owner_proc_pid = ethread.Cid.UniqueProcess
@@ -110,44 +113,52 @@ class ThrdScan(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface)
vollog.debug(f"Thread invalid address {ethread.vol.offset:#x}")
return None
# don't look for VADs in kernel threads, just let them get reported with empty paths
# Filter junk PIDs
if (
owner_proc_pid != 4
and owner_proc.InheritedFromUniqueProcessId != 4
ethread.Cid.UniqueProcess > windows_constants.MAX_PID
or ethread.Cid.UniqueProcess == 0
or ethread.Cid.UniqueProcess % 4 != 0
):
return None
# Get VAD mappings for valid non-system (PID 4) processes
if (
owner_proc
and owner_proc.is_valid()
and owner_proc.UniqueProcessId != 4
and vads_cache is not None
):
vads = pe_symbols.PESymbols.get_vads_for_process_cache(
vads_cache, owner_proc
)
if not vads or len(vads) < 5:
vollog.debug(
f"Not enough vads for process at {owner_proc.vol.offset:#x}. Skipping thread at {ethread.vol.offset:#x}"
)
return None
start_path = pe_symbols.PESymbols.filepath_for_address(
vads, thread_start_addr
start_path = (
pe_symbols.PESymbols.filepath_for_address(vads, thread_start_addr)
if vads
else None
)
win32start_path = pe_symbols.PESymbols.filepath_for_address(
vads, thread_win32start_addr
win32start_path = (
pe_symbols.PESymbols.filepath_for_address(vads, thread_win32start_addr)
if vads
else None
)
else:
start_path = None
win32start_path = None
return (
format_hints.Hex(thread_offset),
return cls.ThreadInfo(
thread_offset,
owner_proc_pid,
thread_tid,
format_hints.Hex(thread_start_addr),
thread_start_addr,
start_path,
format_hints.Hex(thread_win32start_addr),
thread_win32start_addr,
win32start_path,
thread_create_time,
thread_exit_time,
)
def _generator(self, filter_func: Callable):
def _generator(self, filter_func: Callable) -> Iterator[Tuple[int, Tuple]]:
kernel_name = self.config["kernel"]
vads_cache: Dict[int, pe_symbols.ranges_type] = {}
@@ -156,27 +167,16 @@ class ThrdScan(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface)
info = self.gather_thread_info(ethread, vads_cache)
if info:
(
offset,
pid,
tid,
start_addr,
start_path,
win32start_addr,
win32start_path,
create_time,
exit_time,
) = info
yield 0, (
offset,
pid,
tid,
start_addr,
start_path or renderers.NotAvailableValue(),
win32start_addr,
win32start_path or renderers.NotAvailableValue(),
create_time,
exit_time,
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):
@@ -568,16 +568,20 @@ class ETHREAD(objects.StructType, pool.ExecutiveObject):
# passed all validations
return True
def get_create_time(self):
def get_create_time(
self,
) -> Union[datetime.datetime, interfaces.renderers.BaseAbsentValue]:
# For Windows XPs
if self.has_member("ThreadsProcess"):
return conversion.wintime_to_datetime(self.CreateTime.QuadPart >> 3)
return conversion.wintime_to_datetime(self.CreateTime.QuadPart)
def get_exit_time(self):
def get_exit_time(
self,
) -> Union[datetime.datetime, interfaces.renderers.BaseAbsentValue]:
return conversion.wintime_to_datetime(self.ExitTime.QuadPart)
def owning_process(self) -> interfaces.objects.ObjectInterface:
def owning_process(self) -> "EPROCESS":
"""Return the EPROCESS that owns this thread."""
# For Windows XPs
@@ -705,7 +709,11 @@ class EPROCESS(generic.GenericIntelProcess, pool.ExecutiveObject):
return False
# NT pids are divisible by 4
if self.UniqueProcessId % 4 != 0:
if (
self.UniqueProcessId % 4 != 0
or self.UniqueProcessId == 0
or self.UniqueProcessId > constants.windows.MAX_PID
):
return False
# check for all 0s besides the PCID entries