Add the windows plugin and associated extensions updates

This commit is contained in:
Andrew Case
2025-03-15 22:58:07 +00:00
parent 5bd7a4f4c2
commit bad34a112a
14 changed files with 418 additions and 14 deletions
@@ -0,0 +1,137 @@
# This file is Copyright 2025 Volatility Foundation and licensed under the Volatility Software License 1.0
# which is available at https://www.volatilityfoundation.org/license/vsl-v1.0
#
import logging
from typing import List, Iterable
from volatility3.framework import interfaces, renderers, exceptions
from volatility3.framework.objects import utility
from volatility3.framework.configuration import requirements
from volatility3.framework.renderers import format_hints
from volatility3.plugins.windows import windowstations
vollog = logging.getLogger(__name__)
class Windows(interfaces.plugins.PluginInterface):
"""Enumerates the Windows of Desktop instances"""
_required_framework_version = (2, 0, 0)
_version = (1, 0, 0)
@classmethod
def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]:
# Since we're calling the plugin, make sure we have the plugin's requirements
return [
requirements.ModuleRequirement(
name="kernel",
description="Windows kernel",
architectures=["Intel32", "Intel64"],
),
requirements.VersionRequirement(
name="windowstations",
component=windowstations.WindowStations,
version=(1, 0, 0),
),
]
@classmethod
def list_windows(
cls,
context: interfaces.context.ContextInterface,
config_path: str,
kernel_module_name: str,
) -> Iterable[interfaces.objects.ObjectInterface]:
"""
Enumerates the desktops of each window station
For each found, enumerates its windows within the desktop
"""
kernel = context.modules[kernel_module_name]
for (
winsta,
station_name,
session_id,
) in windowstations.WindowStations.scan_window_stations(
context, config_path, kernel_module_name
):
# for each window station, walk its list of desktops
for desktop, desktop_name in winsta.desktops(kernel.symbol_table_name):
try:
top_window = desktop.pDeskInfo.spwnd
except exceptions.InvalidAddressException:
vollog.debug(
f"Desktop with name {desktop_name} in window station {station_name} has a broken window pointer."
)
continue
for window, window_name in desktop.windows(top_window):
yield station_name, desktop_name, window, window_name
def _generator(self):
kernel_name = self.config["kernel"]
# call the implementation for finding windows and gather attributes
for station_name, desktop_name, window, window_name in self.list_windows(
self.context, self.config_path, kernel_name
):
# We need a valid process and session id for the window to display it
process = window.get_process()
process_name = None
if process:
try:
process_name = utility.array_to_string(process.ImageFileName)
process_pid = process.UniqueProcessId
except exceptions.InvalidAddressException:
vollog.debug(
f"Unable to read name and pid of the process for window {window.vol.offset:#x}"
)
if process_name is None:
vollog.warning(
f"Invalid process reference for the process hosting window {window.vol.offset:#x}"
)
continue
sess_id = window.get_session_id()
if sess_id is None:
vollog.debug(
f"Unable to read session id of the process for window {window.vol.offset:#x} in process {process_name}"
)
continue
# procedures can be empty, but if set, should be a valid pointer
window_proc = window.get_window_procedure()
if window_proc is None or window_proc == 0 or window_proc > 0x1000:
window_proc = format_hints.Hex(window_proc)
else:
vollog.warning(
f"Invalid window procedure for the window {window.vol.offset:#x}"
)
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,
)
def run(self):
return renderers.TreeGrid(
[
("Offset", format_hints.Hex),
("Station", str),
("Session", int),
("Desktop", str),
("Window", str),
("Procedure", format_hints.Hex),
("Process", str),
("PID", int),
],
self._generator(),
)
@@ -2,13 +2,17 @@
# which is available at https://www.volatilityfoundation.org/license/vsl-v1.0
#
from typing import Optional, Tuple, Iterator
import logging
from typing import Optional, Tuple, Iterator, Generator
from volatility3.framework import exceptions, constants, interfaces
from volatility3.framework import objects
from volatility3.framework.objects import utility
from volatility3.framework.symbols.windows import extensions
from volatility3.framework.symbols.windows.extensions import pool
vollog = logging.getLogger(__name__)
class tagWINDOWSTATION(objects.StructType, pool.ExecutiveObject):
def is_valid(self) -> bool:
@@ -77,7 +81,7 @@ class tagWINDOWSTATION(objects.StructType, pool.ExecutiveObject):
class tagDESKTOP(objects.StructType, pool.ExecutiveObject):
def is_valid(self) -> bool:
"""
Enforce a valid sid + owning window station
Enforce a valid sid + name
"""
sid = self.get_session_id()
@@ -89,12 +93,18 @@ class tagDESKTOP(objects.StructType, pool.ExecutiveObject):
return False
def get_window_station(self) -> Optional["tagWINDOWSTATION"]:
"""
Attempts to return the window station for this desktop
"""
try:
return self.rpwinstaParent.dereference()
except exceptions.InvalidAddressException:
return None
def get_session_id(self) -> Optional[int]:
"""
Attempts to return the session ID for this desktop
"""
winsta = self.get_window_station()
if winsta:
return winsta.get_session_id()
@@ -120,8 +130,193 @@ class tagDESKTOP(objects.StructType, pool.ExecutiveObject):
yield thread, process_name, process_pid
def _do_get_windows(
self, window, max_windows
) -> Generator[Tuple[interfaces.objects.ObjectInterface, str], None, None]:
"""
Recusively walks and yields the adjacent and child windows
"""
seen_windows = set()
seen_children = set()
if window.vol.offset == 0:
return
yield window, window.get_name()
seen_windows.add(window)
# Walk adjacent windows
while len(seen_windows) < max_windows:
try:
window = window.spwndNext.dereference()
except exceptions.InvalidAddressException:
break
if window.vol.offset == 0:
break
if window.vol.offset in seen_windows:
break
yield window, window.get_name()
seen_windows.add(window)
# Walk children windows and recursively yield them
for window in seen_windows:
child = window
while len(seen_windows) + len(seen_children) < max_windows:
try:
child = child.spwndChild
except exceptions.InvalidAddressException:
break
if child.vol.offset == 0:
break
if child in seen_children:
break
seen_children.add(child)
yield from self._do_get_windows(child, max_windows)
def windows(
self, window, max_windows=10000
) -> Generator[Tuple[interfaces.objects.ObjectInterface, str], None, None]:
"""
Enumerates all windows adjacent to and children of `window`
Args:
window: The window to enumerate windows from
Returns:
A generator of tuples containing the window and its name
"""
seen_windows = set()
for window, window_name in self._do_get_windows(window, max_windows):
if window.vol.offset in seen_windows:
continue
seen_windows.add(window.vol.offset)
yield window, window_name
if len(seen_windows) == max_windows:
break
class tagWND(objects.StructType, pool.ExecutiveObject):
def is_valid(self) -> bool:
"""
Enforce a valid sid
"""
sid = self.get_session_id()
return sid is not None and 0 <= sid < 256
def get_name(self) -> Optional[str]:
"""
directName appeared in later Windows 10 versions and is pointer
strName is a unicode string directly in the structure
"""
if self.has_member("directName"):
try:
return utility.pointer_to_string(
self.directName, count=256, encoding="utf16"
)
except exceptions.InvalidAddressException:
vollog.debug(
f"directname for window at {self.vol.offset:#x} in layer {self.vol.layer_name} is invalid"
)
try:
return self.strName.get_string()
except exceptions.InvalidAddressException:
vollog.debug(
f"strName for window at {self.vol.offset:#x} in layer {self.vol.layer_name} is invalid"
)
return None
def get_session_id(self) -> Optional[int]:
"""
Uses its tagDESKTOP pointer to find its session
"""
desktop = self.get_desktop()
if desktop:
return desktop.get_session_id()
return None
def get_desktop(self) -> Optional[tagDESKTOP]:
"""
Attempts to return the host desktop (tagDESKTOP) for this window
"""
try:
return self.head.rpdesk.dereference()
except exceptions.InvalidAddressException:
vollog.debug(
f"Reading the desktop pointer for window {self.vol.offset:#x} caused a page fault"
)
return None
def get_process(self) -> Optional["extensions.EPROCESS"]:
"""
Attempts to return the host process (_EPROCESS) for this window
"""
try:
return self.head.pti.ppi.Process.dereference()
except exceptions.InvalidAddressException:
vollog.debug(
f"Reading the process pointer for window {self.vol.offset:#x} caused a page fault"
)
return None
def get_window_procedure(self):
"""
Attempts to return the window procedure for this windows
"""
try:
# >= 17134
if hasattr(self, "subPointer"):
return self.subPointer.lpfnWndProc
else:
return self.lpfnWndProc
except exceptions.InvalidAddressException:
vollog.debug(f"Invalid window procedure for window {self.vol.offset:#x}")
return None
# This is copy/paste from UNICODE_STRING in `symbols/windows/extensions/__init__.py`
# The versioning of modules would get very ugly if we let different modules share implementations
# across different data structures
class LARGE_UNICODE_STRING(objects.StructType):
"""A class for Windows unicode string structures."""
def get_string(self) -> interfaces.objects.ObjectInterface:
# We explicitly do *not* catch errors here, we allow an exception to be thrown
# (otherwise there's no way to determine anything went wrong)
# It's up to the user of this method to catch exceptions
# We manually construct an object rather than casting a dereferenced pointer in case
# the buffer length is 0 and the pointer is a NULL pointer
return self._context.object(
self.vol.type_name.split(constants.BANG)[0] + constants.BANG + "string",
layer_name=self.Buffer.vol.native_layer_name,
offset=self.Buffer,
max_length=self.Length,
errors="replace",
encoding="utf16",
)
class_types = {
"tagWINDOWSTATION": tagWINDOWSTATION,
"tagDESKTOP": tagDESKTOP,
"tagWND": tagWND,
"_LARGE_UNICODE_STRING": LARGE_UNICODE_STRING,
}
@@ -18036,6 +18036,12 @@
"signed": false,
"size": 1
},
"char": {
"kind": "char",
"endian": "little",
"signed": false,
"size": 1
},
"float": {
"kind": "float",
"endian": "little",
@@ -18036,6 +18036,12 @@
"signed": false,
"size": 1
},
"char": {
"kind": "char",
"endian": "little",
"signed": false,
"size": 1
},
"float": {
"kind": "float",
"endian": "little",
@@ -18036,6 +18036,12 @@
"signed": false,
"size": 1
},
"char": {
"kind": "char",
"endian": "little",
"signed": false,
"size": 1
},
"float": {
"kind": "float",
"endian": "little",
@@ -12464,8 +12464,8 @@
"directName": {
"type": {
"subtype": {
"kind": "struct",
"name": "nt_symbols!String"
"kind": "base",
"name": "char"
},
"kind": "pointer"
},
@@ -18079,6 +18079,12 @@
"signed": false,
"size": 1
},
"char": {
"kind": "char",
"endian": "little",
"signed": false,
"size": 1
},
"float": {
"kind": "float",
"endian": "little",
@@ -12464,8 +12464,8 @@
"directName": {
"type": {
"subtype": {
"kind": "struct",
"name": "nt_symbols!String"
"kind": "base",
"name": "char"
},
"kind": "pointer"
},
@@ -18079,6 +18079,12 @@
"signed": false,
"size": 1
},
"char": {
"kind": "char",
"endian": "little",
"signed": false,
"size": 1
},
"float": {
"kind": "float",
"endian": "little",
@@ -12464,8 +12464,8 @@
"directName": {
"type": {
"subtype": {
"kind": "struct",
"name": "nt_symbols!String"
"kind": "base",
"name": "char"
},
"kind": "pointer"
},
@@ -18079,6 +18079,12 @@
"signed": false,
"size": 1
},
"char": {
"kind": "char",
"endian": "little",
"signed": false,
"size": 1
},
"float": {
"kind": "float",
"endian": "little",
@@ -12464,8 +12464,8 @@
"directName": {
"type": {
"subtype": {
"kind": "struct",
"name": "nt_symbols!String"
"kind": "base",
"name": "char"
},
"kind": "pointer"
},
@@ -18079,6 +18079,12 @@
"signed": false,
"size": 1
},
"char": {
"kind": "char",
"endian": "little",
"signed": false,
"size": 1
},
"float": {
"kind": "float",
"endian": "little",
@@ -12464,8 +12464,8 @@
"directName": {
"type": {
"subtype": {
"kind": "struct",
"name": "nt_symbols!String"
"kind": "base",
"name": "char"
},
"kind": "pointer"
},
@@ -18079,6 +18079,12 @@
"signed": false,
"size": 1
},
"char": {
"kind": "char",
"endian": "little",
"signed": false,
"size": 1
},
"float": {
"kind": "float",
"endian": "little",
@@ -12464,8 +12464,8 @@
"directName": {
"type": {
"subtype": {
"kind": "struct",
"name": "nt_symbols!String"
"kind": "base",
"name": "char"
},
"kind": "pointer"
},
@@ -18079,6 +18079,12 @@
"signed": false,
"size": 1
},
"char": {
"kind": "char",
"endian": "little",
"signed": false,
"size": 1
},
"float": {
"kind": "float",
"endian": "little",
@@ -18619,6 +18619,12 @@
"signed": false,
"size": 1
},
"char": {
"kind": "char",
"endian": "little",
"signed": false,
"size": 1
},
"float": {
"kind": "float",
"endian": "little",
@@ -17985,6 +17985,12 @@
"signed": false,
"size": 1
},
"char": {
"kind": "char",
"endian": "little",
"signed": false,
"size": 1
},
"float": {
"kind": "float",
"endian": "little",
@@ -17992,6 +17992,12 @@
"signed": false,
"size": 1
},
"char": {
"kind": "char",
"endian": "little",
"signed": false,
"size": 1
},
"float": {
"kind": "float",
"endian": "little",