#816 - initial support for Windows Windows Server 2022 (10.0.20348)

This commit is contained in:
Dave Lassalle
2024-08-01 14:23:22 -05:00
parent 50dbeac080
commit dbc837474c
4 changed files with 2279 additions and 0 deletions
@@ -0,0 +1,786 @@
# 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
#
# This module attempts to locate windows console histories.
import logging
import os
import struct
from typing import Tuple, Generator, Set, Dict, Any, Type
from volatility3.framework import interfaces, symbols, exceptions
from volatility3.framework import renderers
from volatility3.framework.configuration import requirements
from volatility3.framework.layers import scanners
from volatility3.framework.objects import utility
from volatility3.framework.renderers import format_hints
from volatility3.framework.symbols import intermed
from volatility3.framework.symbols.windows import pdbutil, versions
from volatility3.framework.symbols.windows.extensions import pe, consoles
from volatility3.plugins.windows import pslist, vadinfo, info, verinfo
from volatility3.plugins.windows.registry import hivelist
try:
import capstone
has_capstone = True
except ImportError:
has_capstone = False
vollog = logging.getLogger(__name__)
class Consoles(interfaces.plugins.PluginInterface):
"""Looks for Windows console buffers"""
_required_framework_version = (2, 4, 0)
@classmethod
def get_requirements(cls):
# 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="pslist", component=pslist.PsList, version=(2, 0, 0)
),
requirements.VersionRequirement(
name="pdbutil", component=pdbutil.PDBUtility, version=(1, 0, 0)
),
requirements.PluginRequirement(
name="hivelist", plugin=hivelist.HiveList, version=(1, 0, 0)
),
requirements.BooleanRequirement(
name="no_registry",
description="Don't search the registry for possible values of CommandHistorySize and HistoryBufferMax",
optional=True,
default=False,
),
requirements.ListRequirement(
name="max_history",
element_type=int,
description="CommandHistorySize values to search for.",
optional=True,
default=[50],
),
requirements.ListRequirement(
name="max_buffers",
element_type=int,
description="HistoryBufferMax values to search for.",
optional=True,
default=[4],
),
]
@classmethod
def find_conhost_proc(
cls, proc_list: Generator[interfaces.objects.ObjectInterface, None, None]
) -> Tuple[interfaces.context.ContextInterface, str]:
"""
Walks the process list and returns the conhost instances.
Args:
proc_list: The process list generator
Return:
The process object and layer name for conhost
"""
for proc in proc_list:
try:
proc_id = proc.UniqueProcessId
proc_layer_name = proc.add_process_layer()
yield proc, proc_layer_name
except exceptions.InvalidAddressException as excp:
vollog.debug(
"Process {}: invalid address {} in layer {}".format(
proc_id, excp.invalid_address, excp.layer_name
)
)
@classmethod
def find_conhostexe(
cls, conhost_proc: interfaces.context.ContextInterface
) -> Tuple[int, int]:
"""
Finds the base address of conhost.exe
Args:
conhost_proc: the process object for conhost.exe
Returns:
A tuple of:
conhostexe_base: the base address of conhost.exe
conhostexe_size: the size of the VAD for conhost.exe
"""
for vad in conhost_proc.get_vad_root().traverse():
filename = vad.get_file_name()
if isinstance(filename, str) and filename.lower().endswith("conhost.exe"):
base = vad.get_start()
return base, vad.get_size()
return None, None
@classmethod
def determine_conhost_version(
cls,
context: interfaces.context.ContextInterface,
layer_name: str,
nt_symbol_table: str,
) -> Tuple[str, Type]:
"""Tries to determine which symbol filename to use for the image's console information. This is similar to the
netstat plugin.
Args:
context: The context to retrieve required elements (layers, symbol tables) from
layer_name: The name of the layer on which to operate
nt_symbol_table: The name of the table containing the kernel symbols
Returns:
The filename of the symbol table to use and the associated class types.
"""
is_64bit = symbols.symbol_table_is_64bit(context, nt_symbol_table)
is_18363_or_later = versions.is_win10_18363_or_later(
context=context, symbol_table=nt_symbol_table
)
if is_64bit:
arch = "x64"
else:
arch = "x86"
vers = info.Info.get_version_structure(context, layer_name, nt_symbol_table)
kuser = info.Info.get_kuser_structure(context, layer_name, nt_symbol_table)
try:
vers_minor_version = int(vers.MinorVersion)
nt_major_version = int(kuser.NtMajorVersion)
nt_minor_version = int(kuser.NtMinorVersion)
except ValueError:
# vers struct exists, but is not an int anymore?
raise NotImplementedError(
"Kernel Debug Structure version format not supported!"
)
except:
# unsure what to raise here. Also, it might be useful to add some kind of fallback,
# either to a user-provided version or to another method to determine tcpip.sys's version
raise exceptions.VolatilityException(
"Kernel Debug Structure missing VERSION/KUSER structure, unable to determine Windows version!"
)
vollog.debug(
"Determined OS Version: {}.{} {}.{}".format(
kuser.NtMajorVersion,
kuser.NtMinorVersion,
vers.MajorVersion,
vers.MinorVersion,
)
)
if nt_major_version == 10 and arch == "x64":
# win10 x64 has an additional class type we have to include.
class_types = consoles.win10_x64_class_types
else:
# default to general class types
class_types = consoles.class_types
# these versions are listed explicitly because symbol files differ based on
# version *and* architecture. this is currently the clearest way to show
# the differences, even if it introduces a fair bit of redundancy.
# furthermore, it is easy to append new versions.
if arch == "x86":
version_dict = {}
else:
version_dict = {
(10, 0, 20348, 1): "consoles-win10-20348-x64",
(10, 0, 20348, 1970): "consoles-win10-20348-1970-x64",
(10, 0, 20348, 2461): "consoles-win10-20348-2461-x64",
(10, 0, 20348, 2520): "consoles-win10-20348-2461-x64",
}
# we do not need to check for conhost's specific FileVersion in every case
conhost_mod_version = 0 # keep it 0 as a default
# special use cases
# Win10_18363 is not recognized by windows.info as 18363
# because all kernel file headers and debug structures report 18363 as
# "10.0.18362.1198" with the last part being incremented. However, we can use
# os_distinguisher to differentiate between 18362 and 18363
if vers_minor_version == 18362 and is_18363_or_later:
vollog.debug(
"Detected 18363 data structures: working with 18363 symbol table."
)
vers_minor_version = 18363
# we need to define additional version numbers (which are then found via conhost.exe's FileVersion header) in case there is
# ambiguity _within_ an OS version. If such a version number (last number of the tuple) is defined for the current OS
# we need to inspect conhost.exe's headers to see if we can grab the precise version
if [
(a, b, c, d)
for a, b, c, d in version_dict
if (a, b, c) == (nt_major_version, nt_minor_version, vers_minor_version)
and d != 0
]:
vollog.debug(
"Requiring further version inspection due to OS version by checking conhost.exe's FileVersion header"
)
# the following is IntelLayer specific and might need to be adapted to other architectures.
physical_layer_name = context.layers[layer_name].config.get(
"memory_layer", None
)
if physical_layer_name:
ver = verinfo.VerInfo.find_version_info(
context, physical_layer_name, "CONHOST.EXE"
)
if ver:
conhost_mod_version = ver[3]
vollog.debug(
"Determined conhost.exe's FileVersion: {}".format(
conhost_mod_version
)
)
else:
vollog.debug("Could not determine conhost.exe's FileVersion.")
else:
vollog.debug(
"Unable to retrieve physical memory layer, skipping FileVersion check."
)
# when determining the symbol file we have to consider the following cases:
# the determined version's symbol file is found by intermed.create -> proceed
# the determined version's symbol file is not found by intermed -> intermed will throw an exc and abort
# the determined version has no mapped symbol file -> if win10 use latest, otherwise throw exc
# windows version cannot be determined -> throw exc
filename = version_dict.get(
(
nt_major_version,
nt_minor_version,
vers_minor_version,
conhost_mod_version,
)
)
if not filename:
# no match on filename means that we possibly have a version newer than those listed here.
# try to grab the latest supported version of the current image NT version. If that symbol
# version does not work, support has to be added manually.
current_versions = [
(nt_maj, nt_min, vers_min, tcpip_ver)
for nt_maj, nt_min, vers_min, tcpip_ver in version_dict
if nt_maj == nt_major_version
and nt_min == nt_minor_version
and tcpip_ver <= conhost_mod_version
]
current_versions.sort()
if current_versions:
latest_version = current_versions[-1]
filename = version_dict.get(latest_version)
vollog.debug(
f"Unable to find exact matching symbol file, going with latest: {filename}"
)
else:
raise NotImplementedError(
"This version of Windows is not supported: {}.{} {}.{}!".format(
nt_major_version,
nt_minor_version,
vers.MajorVersion,
vers_minor_version,
)
)
vollog.debug(f"Determined symbol filename: {filename}")
return filename, class_types
@classmethod
def create_conhost_symbol_table(
cls,
context: interfaces.context.ContextInterface,
layer_name: str,
nt_symbol_table: str,
config_path: str,
) -> str:
"""Creates a symbol table for TCP Listeners and TCP/UDP Endpoints.
Args:
context: The context to retrieve required elements (layers, symbol tables) from
layer_name: The name of the layer on which to operate
nt_symbol_table: The name of the table containing the kernel symbols
config_path: The config path where to find symbol files
Returns:
The name of the constructed symbol table
"""
table_mapping = {"nt_symbols": nt_symbol_table}
symbol_filename, class_types = cls.determine_conhost_version(
context,
layer_name,
nt_symbol_table,
)
vollog.debug(f"Using symbol file '{symbol_filename}' and types {class_types}")
return intermed.IntermediateSymbolTable.create(
context,
config_path,
os.path.join("windows", "consoles"),
symbol_filename,
class_types=class_types,
table_mapping=table_mapping,
)
@classmethod
def get_console_info(
cls,
context: interfaces.context.ContextInterface,
kernel_layer_name: str,
kernel_table_name: str,
config_path: str,
procs: Generator[interfaces.objects.ObjectInterface, None, None],
max_history: Set[int],
max_buffers: Set[int],
) -> Tuple[
interfaces.context.ContextInterface,
interfaces.context.ContextInterface,
Dict[str, Any],
]:
"""Extracts the cmdline from PEB
Args:
context: The context to retrieve required elements (layers, symbol tables) from
kernel_layer_name: The name of the layer on which to operate
kernel_table_name: The name of the table containing the kernel symbols
config_path: The config path where to find symbol files
procs: list of process objects
max_history: an initial set of CommandHistorySize values
max_buffers: an initial list of HistoryBufferMax values
Returns:
The conhost process object, the console information structure, a dictionary of properties for
that console information structure.
"""
conhost_symbol_table = cls.create_conhost_symbol_table(
context, kernel_layer_name, kernel_table_name, config_path
)
for conhost_proc, proc_layer_name in cls.find_conhost_proc(procs):
if not conhost_proc:
vollog.info(
"Unable to find a valid conhost.exe process in the process list. Analysis cannot proceed."
)
continue
vollog.debug(
f"Found conhost process {conhost_proc} with pid {conhost_proc.UniqueProcessId}"
)
conhostexe_base, conhostexe_size = cls.find_conhostexe(conhost_proc)
if not conhostexe_base:
vollog.info(
"Unable to find the location of conhost.exe. Analysis cannot proceed."
)
continue
vollog.debug(f"Found conhost.exe base at {conhostexe_base:#x}")
proc_layer = context.layers[proc_layer_name]
conhost_module = context.module(
conhost_symbol_table, proc_layer_name, offset=conhostexe_base
)
# scan for potential _CONSOLE_INFORMATION structures by using the CommandHistorySize
for max_history_value in max_history:
max_history_bytes = struct.pack("H", max_history_value)
vollog.debug(
f"Scanning for CommandHistorySize value: {max_history_bytes}"
)
for address in proc_layer.scan(
context,
scanners.BytesScanner(max_history_bytes),
sections=[(conhostexe_base, conhostexe_size)],
):
console_properties = []
try:
console_info = conhost_module.object(
"_CONSOLE_INFORMATION",
offset=address
- conhost_module.get_type(
"_CONSOLE_INFORMATION"
).relative_child_offset("CommandHistorySize"),
absolute=True,
)
if not any(
[
console_info.is_valid(max_buffer)
for max_buffer in max_buffers
]
):
continue
vollog.debug(
f"Getting Console Information properties for {console_info}"
)
console_properties.append(
{
"name": "_CONSOLE_INFORMATION.ScreenX",
"address": console_info.ScreenX.vol.offset,
"data": console_info.ScreenX,
}
)
console_properties.append(
{
"name": "_CONSOLE_INFORMATION.ScreenY",
"address": console_info.ScreenY.vol.offset,
"data": console_info.ScreenY,
}
)
console_properties.append(
{
"name": "_CONSOLE_INFORMATION.CommandHistorySize",
"address": console_info.CommandHistorySize.vol.offset,
"data": console_info.CommandHistorySize,
}
)
console_properties.append(
{
"name": "_CONSOLE_INFORMATION.HistoryBufferCount",
"address": console_info.HistoryBufferCount.vol.offset,
"data": console_info.HistoryBufferCount,
}
)
console_properties.append(
{
"name": "_CONSOLE_INFORMATION.HistoryBufferMax",
"address": console_info.HistoryBufferMax.vol.offset,
"data": console_info.HistoryBufferMax,
}
)
console_properties.append(
{
"name": "_CONSOLE_INFORMATION.Title",
"address": console_info.Title.vol.offset,
"data": console_info.get_title(),
}
)
console_properties.append(
{
"name": "_CONSOLE_INFORMATION.OriginalTitle",
"address": console_info.OriginalTitle.vol.offset,
"data": console_info.get_original_title(),
}
)
vollog.debug(
f"Getting ConsoleProcessList entries for {console_info.ConsoleProcessList}"
)
console_properties.append(
{
"name": "_CONSOLE_INFORMATION.ProcessCount",
"address": console_info.ProcessCount.vol.offset,
"data": console_info.ProcessCount,
}
)
console_properties.append(
{
"name": "_CONSOLE_INFORMATION.ConsoleProcessList",
"address": console_info.ConsoleProcessList.vol.offset,
"data": "",
}
)
for index, attached_proc in enumerate(
console_info.get_processes()
):
console_properties.append(
{
"name": f"_CONSOLE_INFORMATION.ConsoleProcessList.ConsoleProcess_{index}",
"address": attached_proc.ConsoleProcess.dereference().vol.offset,
"data": "",
}
)
console_properties.append(
{
"name": f"_CONSOLE_INFORMATION.ConsoleProcessList.ConsoleProcess_{index}_ProcessId",
"address": attached_proc.ConsoleProcess.ProcessId.vol.offset,
"data": attached_proc.ConsoleProcess.ProcessId,
}
)
console_properties.append(
{
"name": f"_CONSOLE_INFORMATION.ConsoleProcessList.ConsoleProcess_{index}_ProcessHandle",
"address": attached_proc.ConsoleProcess.ProcessHandle.vol.offset,
"data": hex(
attached_proc.ConsoleProcess.ProcessHandle
),
}
)
vollog.debug(
f"Getting HistoryList entries for {console_info.HistoryList}"
)
console_properties.append(
{
"name": "_CONSOLE_INFORMATION.HistoryList",
"address": console_info.HistoryList.vol.offset,
"data": "",
}
)
for index, command_history in enumerate(
console_info.get_histories()
):
try:
console_properties.append(
{
"name": f"_CONSOLE_INFORMATION.HistoryList.CommandHistory_{index}",
"address": command_history.vol.offset,
"data": "",
}
)
console_properties.append(
{
"name": f"_CONSOLE_INFORMATION.HistoryList.CommandHistory_{index}_Application",
"address": command_history.Application.vol.offset,
"data": command_history.get_application(),
}
)
console_properties.append(
{
"name": f"_CONSOLE_INFORMATION.HistoryList.CommandHistory_{index}_ProcessHandle",
"address": command_history.ConsoleProcessHandle.ProcessHandle.vol.offset,
"data": hex(
command_history.ConsoleProcessHandle.ProcessHandle
),
}
)
console_properties.append(
{
"name": f"_CONSOLE_INFORMATION.HistoryList.CommandHistory_{index}_CommandCount",
"address": None,
"data": command_history.CommandCount,
}
)
for (
cmd_index,
bucket_cmd,
) in command_history.get_commands():
try:
console_properties.append(
{
"name": f"_CONSOLE_INFORMATION.HistoryList.CommandHistory_{index}_Command_{cmd_index}",
"address": bucket_cmd.vol.offset,
"data": bucket_cmd.get_command(),
}
)
except Exception as e:
vollog.debug(
f"reading {bucket_cmd} encountered exception {e}"
)
except Exception as e:
vollog.debug(
f"reading {command_history} encountered exception {e}"
)
vollog.debug(f"Getting ScreenBuffer entries for {console_info}")
for screen_index, screen_info in enumerate(
console_info.get_screens()
):
try:
console_properties.append(
{
"name": f"_CONSOLE_INFORMATION.ScreenBuffer_{screen_index}",
"address": screen_info,
"data": "",
}
)
console_properties.append(
{
"name": f"_CONSOLE_INFORMATION.ScreenBuffer_{screen_index}.ScreenX",
"address": None,
"data": screen_info.ScreenX,
}
)
console_properties.append(
{
"name": f"_CONSOLE_INFORMATION.ScreenBuffer_{screen_index}.ScreenY",
"address": None,
"data": screen_info.ScreenY,
}
)
console_properties.append(
{
"name": f"_CONSOLE_INFORMATION.ScreenBuffer_{screen_index}.Dump",
"address": None,
"data": "\n".join(screen_info.get_buffer()),
}
)
except Exception as e:
vollog.debug(
f"reading {screen_info} encountered exception {e}"
)
except exceptions.PagedInvalidAddressException as exp:
vollog.debug(
f"Required memory at {exp.invalid_address:#x} is not valid"
)
yield conhost_proc, console_info, console_properties
@classmethod
def get_console_settings_from_registry(
cls,
context: interfaces.context.ContextInterface,
config_path: str,
kernel_layer_name: str,
kernel_symbol_table_name: str,
max_history: Set[int],
max_buffers: Set[int],
) -> Tuple[Set[int], Set[int]]:
"""
Walks the Registry user hives and extracts any CommandHistorySize and HistoryBufferMax values
for scanning
Args:
context: The context to retrieve required elements (layers, symbol tables) from
config_path: The config path where to find symbol files
kernel_layer_name: The name of the layer on which to operate
kernel_symbol_table_name: The name of the table containing the kernel symbols
max_history: an initial set of CommandHistorySize values
max_buffers: an initial list of HistoryBufferMax values
Returns:
The updated max_history and max_buffers sets.
"""
vollog.debug(
f"Possible CommandHistorySize values before checking Registry: {max_history}"
)
vollog.debug(
f"Possible HistoryBufferMax values before checking Registry: {max_buffers}"
)
for hive in hivelist.HiveList.list_hives(
context=context,
base_config_path=config_path,
layer_name=kernel_layer_name,
symbol_table=kernel_symbol_table_name,
hive_offsets=None,
):
try:
for value in hive.get_key("Console").get_values():
val_name = str(value.get_name())
if val_name == "HistoryBufferSize":
max_history.add(value.decode_data())
elif val_name == "NumberOfHistoryBuffers":
max_buffers.add(value.decode_data())
except:
continue
return max_history, max_buffers
def _generator(
self, procs: Generator[interfaces.objects.ObjectInterface, None, None]
):
"""
Generates the console information to use in rendering
Args:
procs: the process list filtered to conhost.exe instances
"""
kernel = self.context.modules[self.config["kernel"]]
max_history = set(self.config.get("max_history", [50]))
max_buffers = set(self.config.get("max_buffers", [4]))
no_registry = self.config.get("no_registry")
if no_registry is False:
max_history, max_buffers = self.get_console_settings_from_registry(
self.context,
self.config_path,
kernel.layer_name,
kernel.symbol_table_name,
max_history,
max_buffers,
)
vollog.debug(f"Possible CommandHistorySize values: {max_history}")
vollog.debug(f"Possible HistoryBufferMax values: {max_buffers}")
for proc, console_info, console_properties in self.get_console_info(
self.context,
kernel.layer_name,
kernel.symbol_table_name,
self.config_path,
procs,
max_history,
max_buffers,
):
process_name = utility.array_to_string(proc.ImageFileName)
if console_info and console_properties:
for console_property in console_properties:
yield (
0,
(
proc.UniqueProcessId,
process_name,
format_hints.Hex(console_info.vol.offset),
console_property["name"],
(
renderers.NotApplicableValue()
if console_property["address"] is None
else format_hints.Hex(console_property["address"])
),
str(console_property["data"]),
),
)
def _conhost_proc_filter(self, proc):
"""
Used to filter to only conhost.exe processes
"""
process_name = utility.array_to_string(proc.ImageFileName)
return process_name != "conhost.exe"
def run(self):
kernel = self.context.modules[self.config["kernel"]]
return renderers.TreeGrid(
[
("PID", int),
("Process", str),
("ConsoleInfo", format_hints.Hex),
("Property", str),
("Address", format_hints.Hex),
("Data", str),
],
self._generator(
pslist.PsList.list_processes(
context=self.context,
layer_name=kernel.layer_name,
symbol_table=kernel.symbol_table_name,
filter_func=self._conhost_proc_filter,
)
),
)
@@ -0,0 +1,595 @@
{
"symbols": {},
"enums": {},
"base_types": {
"unsigned long": {
"kind": "int",
"size": 4,
"signed": false,
"endian": "little"
},
"unsigned char": {
"kind": "char",
"size": 1,
"signed": false,
"endian": "little"
},
"pointer": {
"kind": "int",
"size": 8,
"signed": false,
"endian": "little"
},
"unsigned int": {
"kind": "int",
"size": 4,
"signed": false,
"endian": "little"
},
"unsigned short": {
"kind": "int",
"size": 2,
"signed": false,
"endian": "little"
},
"short": {
"kind": "int",
"size": 2,
"signed": true,
"endian": "little"
},
"long": {
"kind": "int",
"size": 4,
"signed": false,
"endian": "little"
}
},
"user_types": {
"_CONSOLE_INFORMATION": {
"fields": {
"ScreenX": {
"type": {
"kind": "base",
"name": "short"
},
"offset": 24
},
"ScreenY": {
"type": {
"kind": "base",
"name": "short"
},
"offset": 26
},
"CommandHistorySize": {
"type": {
"kind": "base",
"name": "unsigned short"
},
"offset": 136
},
"HistoryBufferMax": {
"type": {
"kind": "base",
"name": "unsigned short"
},
"offset": 140
},
"OriginalTitle": {
"type": {
"kind": "pointer",
"subtype": {
"kind": "base",
"name": "string"
}
},
"offset": 1616
},
"Title": {
"type": {
"kind": "pointer",
"subtype": {
"kind": "base",
"name": "string"
}
},
"offset": 1552
},
"GetScreenBuffer": {
"type": {
"kind": "pointer",
"subtype": {
"kind": "struct",
"name": "_SCREEN_INFORMATION"
}
},
"offset": 1680
},
"CurrentScreenBuffer": {
"type": {
"kind": "pointer",
"subtype": {
"kind": "struct",
"name": "_SCREEN_INFORMATION"
}
},
"offset": 1296
},
"ConsoleProcessList": {
"type": {
"kind": "pointer",
"subtype": {
"kind": "struct",
"name": "nt_symbols!_LIST_ENTRY"
}
},
"offset": 1272
},
"ProcessCount": {
"type": {
"kind": "base",
"name": "unsigned short"
},
"offset": 1280
},
"HistoryList": {
"type": {
"kind": "pointer",
"subtype": {
"kind": "struct",
"name": "nt_symbols!_LIST_ENTRY"
}
},
"offset": 9176
},
"HistoryBufferCount": {
"type": {
"kind": "base",
"name": "unsigned short"
},
"offset": 9184
},
"ExeAliasList": {
"type": {
"kind": "base",
"name": "unsigned short"
},
"offset": 9232
}
},
"kind": "struct",
"size": 140
},
"_VECTOR": {
"fields": {
"Begin": {
"type": {
"kind": "pointer",
"subtype": {
"kind": "struct",
"name": "_COMMAND"
}
},
"offset": 0
},
"End": {
"type": {
"kind": "pointer",
"subtype": {
"kind": "struct",
"name": "_COMMAND"
}
},
"offset": 8
},
"EndCapacity": {
"type": {
"kind": "pointer",
"subtype": {
"kind": "base",
"name": "unsigned long"
}
},
"offset": 16
}
},
"kind": "struct",
"size": 24
},
"_COMMAND": {
"fields": {
"Chars": {
"type": {
"count": 1,
"kind": "array",
"subtype": {
"kind": "base",
"name": "unsigned char"
}
},
"offset": 0
},
"Pointer": {
"type": {
"kind": "pointer",
"subtype": {
"kind": "base",
"name": "string"
}
},
"offset": 0
},
"Length": {
"type": {
"kind": "base",
"name": "unsigned int"
},
"offset": 16
},
"Allocated": {
"type": {
"kind": "base",
"name": "unsigned int"
},
"offset": 20
}
},
"kind": "struct",
"size": 32
},
"_CONSOLE_PROCESS_LIST": {
"fields": {
"ListEntry": {
"type": {
"kind": "struct",
"name": "nt_symbols!_LIST_ENTRY"
},
"offset": 0
},
"ConsoleProcess": {
"type": {
"kind": "pointer",
"subtype": {
"kind": "struct",
"name": "_CONSOLE_PROCESS"
}
},
"offset": 16
}
},
"kind": "struct",
"size": 24
},
"_CONSOLE_PROCESS_HANDLE": {
"fields": {
"ProcessHandle": {
"type": {
"kind": "base",
"name": "unsigned int"
},
"offset": 48
}
},
"kind": "struct",
"size": 52
},
"_CONSOLE_PROCESS": {
"fields": {
"ProcessId": {
"type": {
"kind": "base",
"name": "unsigned int"
},
"offset": 28
},
"ThreadId": {
"type": {
"kind": "base",
"name": "unsigned int"
},
"offset": 32
},
"ProcessHandle": {
"type": {
"kind": "base",
"name": "unsigned int"
},
"offset": 48
}
},
"kind": "struct",
"size": 24
},
"_COMMAND_HISTORY": {
"fields": {
"ListEntry": {
"type": {
"kind": "struct",
"name": "nt_symbols!_LIST_ENTRY"
},
"offset": 0
},
"CommandBucket": {
"type": {
"kind": "struct",
"name": "_VECTOR"
},
"offset": 16
},
"CommandCountMax": {
"type": {
"kind": "base",
"name": "unsigned short"
},
"offset": 40
},
"Application": {
"type": {
"kind": "struct",
"name": "_COMMAND"
},
"offset": 48
},
"ConsoleProcessHandle": {
"type": {
"kind": "pointer",
"subtype": {
"kind": "struct",
"name": "_CONSOLE_PROCESS_HANDLE"
}
},
"offset": 80
},
"Flags": {
"type": {
"kind": "base",
"name": "unsigned short"
},
"offset": 88
},
"LastDisplayed": {
"type": {
"kind": "base",
"name": "short"
},
"offset": 92
}
},
"kind": "struct",
"size": 96
},
"_SCREEN_INFORMATION": {
"fields": {
"TextBufferInfo": {
"type": {
"kind": "pointer",
"subtype": {
"kind": "struct",
"name": "_TEXT_BUFFER_INFO"
}
},
"offset": 56
},
"Next": {
"type": {
"kind": "pointer",
"subtype": {
"kind": "struct",
"name": "_SCREEN_INFORMATION"
}
},
"offset": 64
}
},
"kind": "struct",
"size": 72
},
"_ROW_POINTER": {
"fields": {
"Row": {
"type": {
"kind": "pointer",
"subtype": {
"kind": "struct",
"name": "_ROW"
}
},
"offset": 0
}
},
"kind": "struct",
"size": 8
},
"_ROWS_ARRAY": {
"fields": {
"Rows": {
"type": {
"count": 1,
"kind": "array",
"subtype": {
"kind": "struct",
"name": "_ROW_POINTER"
}
},
"offset": 0
}
},
"kind": "struct",
"size": 8
},
"_TEXT_BUFFER_INFO": {
"fields": {
"BufferDeque": {
"type": {
"kind": "pointer",
"subtype": {
"kind": "struct",
"name": "_DEQUE"
}
},
"offset": 0
},
"BufferRows": {
"type": {
"kind": "pointer",
"name": "void",
"subtype": {
"kind": "struct",
"name": "_ROWS_ARRAY"
}
},
"offset": 16
},
"BufferCapacity": {
"type": {
"kind": "base",
"name": "short"
},
"offset": 40
},
"ThisBuffer": {
"type": {
"kind": "pointer",
"subtype": {
"kind": "struct",
"name": "_TEXT_BUFFER_INFO"
}
},
"offset": 48
},
"BufferEnd": {
"type": {
"kind": "base",
"name": "short"
},
"offset": 54
},
"BufferLastIndex": {
"type": {
"kind": "base",
"name": "short"
},
"offset": 56
},
"BufferStart": {
"type": {
"kind": "base",
"name": "short"
},
"offset": 96
}
},
"kind": "struct",
"size": 72
},
"_CHAR_ROW_CELL": {
"fields": {
"Text": {
"type": {
"kind": "struct",
"name": "nt_symbols!_UNICODE_STRING"
},
"offset": 0
},
"DbcsAttribute": {
"type": {
"kind": "base",
"name": "unsigned int"
},
"offset": 2
}
},
"kind": "struct",
"size": 3
},
"_CHAR_ROW_CELL_ARRAY": {
"fields": {
"Chars": {
"type": {
"count": 1,
"kind": "array",
"subtype": {
"kind": "struct",
"name": "_CHAR_ROW_CELL"
}
},
"offset": 0
}
},
"kind": "struct",
"size": 8
},
"_ROW": {
"fields": {
"CharRow": {
"type": {
"kind": "pointer",
"subtype": {
"kind": "struct",
"name": "_CHAR_ROW_CELL_ARRAY"
}
},
"offset": 8
},
"RowLength": {
"type": {
"kind": "base",
"name": "short"
},
"offset": 64
},
"Index": {
"type": {
"kind": "base",
"name": "short"
},
"offset": 72
},
"RowLength2": {
"type": {
"kind": "base",
"name": "short"
},
"offset": 80
},
"TextBuffer": {
"type": {
"kind": "pointer",
"subtype": {
"kind": "struct",
"name": "_TEXT_BUFFER_INFO"
}
},
"offset": 88
} },
"kind": "struct",
"size": 96
},
"_DEQUE": {
"fields": {
"Blink": {
"type": {
"kind": "pointer",
"subtype": {
"kind": "base",
"name": "void"
}
},
"offset": 0
}
},
"kind": "struct",
"size": 8
}
},
"metadata": {
"producer": {
"version": "0.0.1",
"name": "Dave Lassalle",
"datetime": "2024-07-31T15:05:35-06:00"
},
"format": "4.1.0"
}
}
@@ -0,0 +1,595 @@
{
"symbols": {},
"enums": {},
"base_types": {
"unsigned long": {
"kind": "int",
"size": 4,
"signed": false,
"endian": "little"
},
"unsigned char": {
"kind": "char",
"size": 1,
"signed": false,
"endian": "little"
},
"pointer": {
"kind": "int",
"size": 8,
"signed": false,
"endian": "little"
},
"unsigned int": {
"kind": "int",
"size": 4,
"signed": false,
"endian": "little"
},
"unsigned short": {
"kind": "int",
"size": 2,
"signed": false,
"endian": "little"
},
"short": {
"kind": "int",
"size": 2,
"signed": true,
"endian": "little"
},
"long": {
"kind": "int",
"size": 4,
"signed": false,
"endian": "little"
}
},
"user_types": {
"_CONSOLE_INFORMATION": {
"fields": {
"ScreenX": {
"type": {
"kind": "base",
"name": "short"
},
"offset": 24
},
"ScreenY": {
"type": {
"kind": "base",
"name": "short"
},
"offset": 26
},
"CommandHistorySize": {
"type": {
"kind": "base",
"name": "unsigned short"
},
"offset": 136
},
"HistoryBufferMax": {
"type": {
"kind": "base",
"name": "unsigned short"
},
"offset": 140
},
"OriginalTitle": {
"type": {
"kind": "pointer",
"subtype": {
"kind": "base",
"name": "string"
}
},
"offset": 1616
},
"Title": {
"type": {
"kind": "pointer",
"subtype": {
"kind": "base",
"name": "string"
}
},
"offset": 1552
},
"GetScreenBuffer": {
"type": {
"kind": "pointer",
"subtype": {
"kind": "struct",
"name": "_SCREEN_INFORMATION"
}
},
"offset": 1680
},
"CurrentScreenBuffer": {
"type": {
"kind": "pointer",
"subtype": {
"kind": "struct",
"name": "_SCREEN_INFORMATION"
}
},
"offset": 1296
},
"ConsoleProcessList": {
"type": {
"kind": "pointer",
"subtype": {
"kind": "struct",
"name": "nt_symbols!_LIST_ENTRY"
}
},
"offset": 1272
},
"ProcessCount": {
"type": {
"kind": "base",
"name": "unsigned short"
},
"offset": 1280
},
"HistoryList": {
"type": {
"kind": "pointer",
"subtype": {
"kind": "struct",
"name": "nt_symbols!_LIST_ENTRY"
}
},
"offset": -288
},
"HistoryBufferCount": {
"type": {
"kind": "base",
"name": "unsigned short"
},
"offset": -280
},
"ExeAliasList": {
"type": {
"kind": "base",
"name": "unsigned short"
},
"offset": -376
}
},
"kind": "struct",
"size": 140
},
"_VECTOR": {
"fields": {
"Begin": {
"type": {
"kind": "pointer",
"subtype": {
"kind": "struct",
"name": "_COMMAND"
}
},
"offset": 0
},
"End": {
"type": {
"kind": "pointer",
"subtype": {
"kind": "struct",
"name": "_COMMAND"
}
},
"offset": 8
},
"EndCapacity": {
"type": {
"kind": "pointer",
"subtype": {
"kind": "base",
"name": "unsigned long"
}
},
"offset": 16
}
},
"kind": "struct",
"size": 24
},
"_COMMAND": {
"fields": {
"Chars": {
"type": {
"count": 1,
"kind": "array",
"subtype": {
"kind": "base",
"name": "unsigned char"
}
},
"offset": 0
},
"Pointer": {
"type": {
"kind": "pointer",
"subtype": {
"kind": "base",
"name": "string"
}
},
"offset": 0
},
"Length": {
"type": {
"kind": "base",
"name": "unsigned int"
},
"offset": 16
},
"Allocated": {
"type": {
"kind": "base",
"name": "unsigned int"
},
"offset": 20
}
},
"kind": "struct",
"size": 32
},
"_CONSOLE_PROCESS_LIST": {
"fields": {
"ListEntry": {
"type": {
"kind": "struct",
"name": "nt_symbols!_LIST_ENTRY"
},
"offset": 0
},
"ConsoleProcess": {
"type": {
"kind": "pointer",
"subtype": {
"kind": "struct",
"name": "_CONSOLE_PROCESS"
}
},
"offset": 16
}
},
"kind": "struct",
"size": 24
},
"_CONSOLE_PROCESS_HANDLE": {
"fields": {
"ProcessHandle": {
"type": {
"kind": "base",
"name": "unsigned int"
},
"offset": 48
}
},
"kind": "struct",
"size": 52
},
"_CONSOLE_PROCESS": {
"fields": {
"ProcessId": {
"type": {
"kind": "base",
"name": "unsigned int"
},
"offset": 28
},
"ThreadId": {
"type": {
"kind": "base",
"name": "unsigned int"
},
"offset": 32
},
"ProcessHandle": {
"type": {
"kind": "base",
"name": "unsigned int"
},
"offset": 48
}
},
"kind": "struct",
"size": 24
},
"_COMMAND_HISTORY": {
"fields": {
"ListEntry": {
"type": {
"kind": "struct",
"name": "nt_symbols!_LIST_ENTRY"
},
"offset": 0
},
"CommandBucket": {
"type": {
"kind": "struct",
"name": "_VECTOR"
},
"offset": 16
},
"CommandCountMax": {
"type": {
"kind": "base",
"name": "unsigned short"
},
"offset": 40
},
"Application": {
"type": {
"kind": "struct",
"name": "_COMMAND"
},
"offset": 48
},
"ConsoleProcessHandle": {
"type": {
"kind": "pointer",
"subtype": {
"kind": "struct",
"name": "_CONSOLE_PROCESS_HANDLE"
}
},
"offset": 80
},
"Flags": {
"type": {
"kind": "base",
"name": "unsigned short"
},
"offset": 88
},
"LastDisplayed": {
"type": {
"kind": "base",
"name": "short"
},
"offset": 92
}
},
"kind": "struct",
"size": 96
},
"_SCREEN_INFORMATION": {
"fields": {
"TextBufferInfo": {
"type": {
"kind": "pointer",
"subtype": {
"kind": "struct",
"name": "_TEXT_BUFFER_INFO"
}
},
"offset": 56
},
"Next": {
"type": {
"kind": "pointer",
"subtype": {
"kind": "struct",
"name": "_SCREEN_INFORMATION"
}
},
"offset": 64
}
},
"kind": "struct",
"size": 72
},
"_ROW_POINTER": {
"fields": {
"Row": {
"type": {
"kind": "pointer",
"subtype": {
"kind": "struct",
"name": "_ROW"
}
},
"offset": 0
}
},
"kind": "struct",
"size": 8
},
"_ROWS_ARRAY": {
"fields": {
"Rows": {
"type": {
"count": 1,
"kind": "array",
"subtype": {
"kind": "struct",
"name": "_ROW_POINTER"
}
},
"offset": 0
}
},
"kind": "struct",
"size": 8
},
"_TEXT_BUFFER_INFO": {
"fields": {
"BufferDeque": {
"type": {
"kind": "pointer",
"subtype": {
"kind": "struct",
"name": "_DEQUE"
}
},
"offset": 0
},
"BufferRows": {
"type": {
"kind": "pointer",
"name": "void",
"subtype": {
"kind": "struct",
"name": "_ROWS_ARRAY"
}
},
"offset": 16
},
"BufferCapacity": {
"type": {
"kind": "base",
"name": "short"
},
"offset": 40
},
"ThisBuffer": {
"type": {
"kind": "pointer",
"subtype": {
"kind": "struct",
"name": "_TEXT_BUFFER_INFO"
}
},
"offset": 48
},
"BufferEnd": {
"type": {
"kind": "base",
"name": "short"
},
"offset": 54
},
"BufferLastIndex": {
"type": {
"kind": "base",
"name": "short"
},
"offset": 56
},
"BufferStart": {
"type": {
"kind": "base",
"name": "short"
},
"offset": 96
}
},
"kind": "struct",
"size": 72
},
"_CHAR_ROW_CELL": {
"fields": {
"Text": {
"type": {
"kind": "struct",
"name": "nt_symbols!_UNICODE_STRING"
},
"offset": 0
},
"DbcsAttribute": {
"type": {
"kind": "base",
"name": "unsigned int"
},
"offset": 2
}
},
"kind": "struct",
"size": 3
},
"_CHAR_ROW_CELL_ARRAY": {
"fields": {
"Chars": {
"type": {
"count": 1,
"kind": "array",
"subtype": {
"kind": "struct",
"name": "_CHAR_ROW_CELL"
}
},
"offset": 0
}
},
"kind": "struct",
"size": 8
},
"_ROW": {
"fields": {
"CharRow": {
"type": {
"kind": "pointer",
"subtype": {
"kind": "struct",
"name": "_CHAR_ROW_CELL_ARRAY"
}
},
"offset": 8
},
"RowLength": {
"type": {
"kind": "base",
"name": "short"
},
"offset": 64
},
"Index": {
"type": {
"kind": "base",
"name": "short"
},
"offset": 72
},
"RowLength2": {
"type": {
"kind": "base",
"name": "short"
},
"offset": 80
},
"TextBuffer": {
"type": {
"kind": "pointer",
"subtype": {
"kind": "struct",
"name": "_TEXT_BUFFER_INFO"
}
},
"offset": 88
} },
"kind": "struct",
"size": 96
},
"_DEQUE": {
"fields": {
"Blink": {
"type": {
"kind": "pointer",
"subtype": {
"kind": "base",
"name": "void"
}
},
"offset": 0
}
},
"kind": "struct",
"size": 8
}
},
"metadata": {
"producer": {
"version": "0.0.1",
"name": "Dave Lassalle",
"datetime": "2024-07-31T15:05:35-06:00"
},
"format": "4.1.0"
}
}
@@ -0,0 +1,303 @@
# 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 volatility3.framework import objects
from volatility3.framework import constants
class ROW(objects.StructType):
"""A Row Structure."""
def _valid_dbcs(self, c):
# TODO this need more research and testing
# https://github.com/search?q=repo%3Amicrosoft%2Fterminal+DbcsAttr&type=code
valid = c in (
0x0,
0x1,
0x2,
0x20,
0x28,
0x30,
0x48,
0x60,
0x80,
0xF8,
0xF0,
0xA0,
)
# if not valid:
# print("Bad Dbcs Attribute {}".format(hex(c)))
return valid
def get_text(self, truncate=True):
"""A convenience method to extract the text from the _ROW. The _ROW
contains a pointer CharRow to an array of CharRowCell objects. Each
CharRowCell contains the wide character and an attribute. Enumerating
self.CharRow.Chars and casting each character to unicode takes too long,
so this reads the whole row into a buffer, then extracts the text characters."""
layer = self._context.layers[self.vol.layer_name]
offset = self.CharRow.Chars.vol.offset
length = self.RowLength * 3
char_row = layer.read(offset, length)
line = ""
try:
if char_row:
line = "".join(
# "{} {} =".format(char_row[i:i + 2].decode('utf-16le', errors='replace'), char_row[i+2]) if self._valid_dbcs(char_row[i+2]) else ""
(
char_row[i : i + 2].decode("utf-16le", errors="replace")
if self._valid_dbcs(char_row[i + 2])
else ""
)
for i in range(0, len(char_row), 3)
)
except Exception as e:
print(e)
line = ""
if truncate:
return line.rstrip()
else:
return line
class SCREEN_INFORMATION(objects.StructType):
"""A Screen Information Structure."""
@property
def ScreenX(self):
return self.TextBufferInfo.BufferRows.Rows[0].Row.RowLength2
@property
def ScreenY(self):
return self.TextBufferInfo.BufferCapacity
def _truncate_rows(self, rows):
"""To truncate empty rows at the end, walk the list
backwards and get the last non-empty row. Use that
row index to splice. Rows are created based on the
length given in the ROW structure, so empty rows will
be ''."""
non_empty_index = 0
rows_traversed = False
for index, row in enumerate(reversed(rows)):
# the string was created based on the length in the ROW structure so it shouldn't have any bad data
if len(row.rstrip()) > 0:
non_empty_index = index
break
rows_traversed = True
if non_empty_index == 0 and rows_traversed:
rows = []
else:
rows = rows[0 : len(rows) - non_empty_index]
return rows
def get_buffer(self, truncate_rows=True, truncate_lines=True):
"""Get the screen buffer.
The screen buffer is comprised of the screen's Y
coordinate which tells us the number of rows and
the X coordinate which tells us the width of each
row in characters. Windows 10 17763 changed from
a large text buffer to a grid of cells, with each
cell containing a single wide character in that
cell, stored in a CharRowCell object.
@param truncate: True if the empty rows at the
end (i.e. bottom) of the screen buffer should be
supressed.
"""
rows = []
capacity = self.TextBufferInfo.BufferCapacity
start = self.TextBufferInfo.BufferStart
buffer_rows = self.TextBufferInfo.BufferRows.dereference()
buffer_rows.Rows.count = self.TextBufferInfo.BufferCapacity
for i in range(capacity):
index = (start + i) % capacity
row = buffer_rows.Rows[index].Row
try:
text = row.get_text(truncate_lines)
rows.append(text)
except:
break
if truncate_rows:
rows = self._truncate_rows(rows)
if rows:
rows = ["=== Start of buffer ==="] + rows + ["=== End of buffer ==="]
else:
rows = ["=== No buffer data found ==="]
return rows
class CONSOLE_INFORMATION(objects.StructType):
"""A Console Information Structure."""
@property
def ScreenBuffer(self):
return self.GetScreenBuffer
def is_valid(self, max_buffers=4) -> bool:
"""Determine if the structure is valid."""
# Last displayed must be between -1 and max
if self.HistoryBufferCount < 1 or self.HistoryBufferCount > max_buffers:
return False
if not self.get_title() and not self.get_original_title():
return False
return True
def get_screens(self):
"""Generator for screens in the console.
A console can have multiple screen buffers at a time,
but only the current/active one is displayed.
Multiple screens are tracked using the singly-linked
list _SCREEN_INFORMATION.Next.
See CreateConsoleScreenBuffer
"""
screens = [self.CurrentScreenBuffer]
if self.ScreenBuffer not in screens:
screens.append(self.ScreenBuffer)
seen = set()
for screen in screens:
cur = screen
while cur and cur.vol.offset != 0 and cur.vol.offset not in seen:
cur.TextBufferInfo.BufferRows.Rows.count = (
cur.TextBufferInfo.BufferCapacity
)
yield cur
seen.add(cur.vol.offset)
cur = cur.Next
def get_histories(self):
for cmd_hist in self.HistoryList.dereference().to_list(
f"{self.get_symbol_table_name()}{constants.BANG}_COMMAND_HISTORY",
"ListEntry",
):
yield cmd_hist
def get_processes(self):
for proc in self.ConsoleProcessList.dereference().to_list(
f"{self.get_symbol_table_name()}{constants.BANG}_CONSOLE_PROCESS_LIST",
"ListEntry",
):
yield proc
def get_title(self):
try:
return self.Title.dereference().cast(
"string", encoding="utf-16", errors="replace", max_length=512
)
except:
return ""
def get_original_title(self):
try:
return self.OriginalTitle.dereference().cast(
"string", encoding="utf-16", errors="replace", max_length=512
)
except:
return ""
class COMMAND(objects.StructType):
"""A Command Structure"""
def get_command(self):
if self.Length < 8:
return self.Chars.cast(
"string",
encoding="utf-16",
errors="replace",
max_length=self.Length * 2,
)
elif self.Length < 1024:
return self.Pointer.dereference().cast(
"string", encoding="utf-16", errors="replace", max_length=512
)
class COMMAND_HISTORY(objects.StructType):
"""A Command History Structure."""
@property
def CommandCount(self):
command_type = self.get_symbol_table_name() + constants.BANG + "_COMMAND"
command_size = self._context.symbol_space.get_type(command_type).size
return int((self.CommandBucket.End - self.CommandBucket.Begin) / command_size)
def get_application(self):
if self.Application.Length < 8:
return self.Application.Chars.cast(
"string",
encoding="utf-16",
errors="replace",
max_length=self.Application.Length * 2,
)
elif self.Application.Length < 1024:
return self.Application.Pointer.dereference().cast(
"string", encoding="utf-16", errors="replace", max_length=512
)
def scan_command_bucket(self, end=None):
"""Brute force print all strings pointed to by the CommandBucket entries by
going to greater of EndCapacity or CommandCountMax*sizeof(_COMMAND)"""
command_type = self.get_symbol_table_name() + constants.BANG + "_COMMAND"
command_history_size = self._context.symbol_space.get_type(
self.vol.type_name
).size
command_size = self._context.symbol_space.get_type(command_type).size
if end is None:
end = max(
self.CommandBucket.EndCapacity,
self.CommandBucket.Begin + command_history_size * self.CommandCountMax,
)
for i, pointer in enumerate(range(self.CommandBucket.Begin, end, command_size)):
yield i, self._context.object(command_type, self.vol.layer_name, pointer)
def get_commands(self):
"""Generator for commands in the history buffer.
The CommandBucket is an array of pointers to _COMMAND
structures. The array size is CommandCount. Once CommandCount
is reached, the oldest commands are cycled out and the
rest are coalesced.
"""
for i, cmd in self.scan_command_bucket(self.CommandBucket.End):
yield i, cmd
win10_x64_class_types = {
"_ROW": ROW,
"_SCREEN_INFORMATION": SCREEN_INFORMATION,
"_CONSOLE_INFORMATION": CONSOLE_INFORMATION,
"_COMMAND_HISTORY": COMMAND_HISTORY,
"_COMMAND": COMMAND,
}
class_types = {
"_ROW": ROW,
"_SCREEN_INFORMATION": SCREEN_INFORMATION,
"_CONSOLE_INFORMATION": CONSOLE_INFORMATION,
"_COMMAND_HISTORY": COMMAND_HISTORY,
"_COMMAND": COMMAND,
}