mirror of
https://github.com/volatilityfoundation/volatility3.git
synced 2026-08-17 20:35:40 +02:00
plugins can choose from a list of built-ins or they can create their own and pass them into generate_pool_scan()
353 lines
16 KiB
Python
353 lines
16 KiB
Python
# This file was contributed to the Volatility Framework Version 3.
|
|
# Copyright (C) 2018 Volatility Foundation.
|
|
#
|
|
# THE LICENSED WORK IS PROVIDED UNDER THE TERMS OF THE Volatility Contributors
|
|
# Public License V1.0("LICENSE") AS FIRST COMPLETED BY: Volatility Foundation,
|
|
# Inc. ANY USE, PUBLIC DISPLAY, PUBLIC PERFORMANCE, REPRODUCTION OR DISTRIBUTION
|
|
# OF, OR PREPARATION OF SUBSEQUENT WORKS, DERIVATIVE WORKS OR DERIVED WORKS BASED
|
|
# ON, THE LICENSED WORK CONSTITUTES RECIPIENT'S ACCEPTANCE OF THIS LICENSE AND ITS
|
|
# TERMS, WHETHER OR NOT SUCH RECIPIENT READS THE TERMS OF THE LICENSE. "LICENSED
|
|
# WORK,” “RECIPIENT" AND “DISTRIBUTOR" ARE DEFINED IN THE LICENSE. A COPY OF THE
|
|
# LICENSE IS LOCATED IN THE TEXT FILE ENTITLED "LICENSE.txt" ACCOMPANYING THE
|
|
# CONTENTS OF THIS FILE. IF A COPY OF THE LICENSE DOES NOT ACCOMPANY THIS FILE, A
|
|
# COPY OF THE LICENSE MAY ALSO BE OBTAINED AT THE FOLLOWING WEB SITE:
|
|
# https://www.volatilityfoundation.org/license/vcpl_v1.0
|
|
#
|
|
# Software distributed under the License is distributed on an "AS IS" basis,
|
|
# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for the
|
|
# specific language governing rights and limitations under the License.
|
|
#
|
|
|
|
import enum
|
|
import logging
|
|
from typing import Dict, Generator, List, Optional, Tuple
|
|
|
|
import volatility.plugins.windows.handles as handles
|
|
|
|
from volatility.framework import constants, interfaces, renderers, exceptions, symbols
|
|
from volatility.framework.configuration import requirements
|
|
from volatility.framework.interfaces import plugins, configuration
|
|
from volatility.framework.layers import scanners
|
|
from volatility.framework.renderers import format_hints
|
|
from volatility.framework.symbols import intermed
|
|
from volatility.framework.symbols.windows import extensions
|
|
|
|
vollog = logging.getLogger(__name__)
|
|
|
|
|
|
class PoolType(enum.IntEnum):
|
|
"""Class to maintain the different possible PoolTypes
|
|
The values must be integer powers of 2"""
|
|
|
|
PAGED = 1
|
|
NONPAGED = 2
|
|
FREE = 4
|
|
|
|
|
|
class PoolHeaderSymbolTable(intermed.IntermediateSymbolTable):
|
|
|
|
def __init__(self, *args, **kwargs) -> None:
|
|
super().__init__(*args, **kwargs)
|
|
self.set_type_class('_POOL_HEADER', extensions._POOL_HEADER)
|
|
|
|
|
|
class PoolConstraint:
|
|
"""Class to maintain tag/size/index/type information about Pool header tags"""
|
|
|
|
def __init__(self,
|
|
tag: bytes,
|
|
type_name: str,
|
|
object_type: Optional[str] = None,
|
|
page_type: Optional[PoolType] = None,
|
|
size: Optional[Tuple[Optional[int], Optional[int]]] = None,
|
|
index: Optional[Tuple[Optional[int], Optional[int]]] = None,
|
|
alignment: Optional[int] = 1) -> None:
|
|
self.tag = tag
|
|
self.type_name = type_name
|
|
self.object_type = object_type
|
|
self.page_type = page_type
|
|
self.size = size
|
|
self.index = index
|
|
self.alignment = alignment
|
|
|
|
|
|
class PoolScanner(plugins.PluginInterface):
|
|
"""A generic pool scanner plugin"""
|
|
|
|
@classmethod
|
|
def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]:
|
|
return [
|
|
requirements.TranslationLayerRequirement(
|
|
name = 'primary', description = 'Memory layer for the kernel', architectures = ["Intel32", "Intel64"]),
|
|
requirements.SymbolTableRequirement(name = "nt_symbols", description = "Windows kernel symbols")
|
|
]
|
|
|
|
@staticmethod
|
|
def is_windows_10(context: interfaces.context.ContextInterface, symbol_table: str) -> bool:
|
|
"""Determine if the analyzed sample is Windows 10"""
|
|
|
|
# try the primary method based on the pe version in the ISF
|
|
try:
|
|
pe_version = context.symbol_space[symbol_table].metadata.pe_version
|
|
major, minor, _revision, _build = pe_version
|
|
return (major, minor) >= (10, 0)
|
|
except (AttributeError, ValueError):
|
|
vollog.log(constants.LOGLEVEL_VVV, "Windows PE version data is not available")
|
|
|
|
# fall back to the backup method, if necessary
|
|
try:
|
|
_symbol = context.symbol_space.get_symbol(symbol_table + constants.BANG + "ObHeaderCookie")
|
|
return True
|
|
except exceptions.SymbolError:
|
|
return False
|
|
|
|
@staticmethod
|
|
def is_windows_8_or_later(context: interfaces.context.ContextInterface, layer_name: str, symbol_table: str) -> bool:
|
|
"""Determine if the analyzed sample is Windows 8 or later"""
|
|
|
|
# try the primary method based on the pe version in the ISF
|
|
try:
|
|
pe_version = context.symbol_space[symbol_table].metadata.pe_version
|
|
major, minor, _revision, _build = pe_version
|
|
return (major, minor) >= (6, 2)
|
|
except (AttributeError, ValueError):
|
|
vollog.log(constants.LOGLEVEL_VVV, "Windows PE version data is not available")
|
|
|
|
# fall back to the backup method, if necessary
|
|
kvo = context.memory[layer_name].config['kernel_virtual_offset']
|
|
ntkrnlmp = context.module(symbol_table, layer_name = layer_name, offset = kvo)
|
|
handle_table_type = ntkrnlmp.get_type("_HANDLE_TABLE")
|
|
return not handle_table_type.has_member("HandleCount")
|
|
|
|
@staticmethod
|
|
def is_windows_7(context: interfaces.context.ContextInterface, layer_name: str, symbol_table: str) -> bool:
|
|
"""Determine if the analyzed sample is Windows 7"""
|
|
|
|
# try the primary method based on the pe version in the ISF
|
|
try:
|
|
pe_version = context.symbol_space[symbol_table].metadata.pe_version
|
|
major, minor, _revision, _build = pe_version
|
|
return (major, minor) == (6, 1)
|
|
except (AttributeError, ValueError):
|
|
vollog.log(constants.LOGLEVEL_VVV, "Windows PE version data is not available")
|
|
|
|
# fall back to the backup method, if necessary
|
|
kvo = context.memory[layer_name].config['kernel_virtual_offset']
|
|
ntkrnlmp = context.module(symbol_table, layer_name = layer_name, offset = kvo)
|
|
handle_table_type = ntkrnlmp.get_type("_OBJECT_HEADER")
|
|
return (handle_table_type.has_member("TypeIndex")
|
|
and not PoolScanner.is_windows_8_or_later(context, layer_name, symbol_table))
|
|
|
|
def _generator(self):
|
|
|
|
symbol_table = self.config["nt_symbols"]
|
|
constraints = self.builtin_constraints(symbol_table, [
|
|
b'AtmT',
|
|
b'Pro\xe3',
|
|
b'Proc',
|
|
b'Fil\xe5',
|
|
b'File',
|
|
])
|
|
|
|
for result in self.generate_pool_scan(self.context,
|
|
self.config["primary"],
|
|
symbol_table,
|
|
constraints):
|
|
|
|
constraint, mem_object, header = result
|
|
|
|
# generate some type-specific info for sanity checking
|
|
if constraint.object_type == "Process":
|
|
name = mem_object.ImageFileName.cast(
|
|
"string", max_length=mem_object.ImageFileName.vol.count, errors="replace")
|
|
elif constraint.object_type == "File":
|
|
try:
|
|
name = mem_object.FileName.String
|
|
except exceptions.PagedInvalidAddressException:
|
|
vollog.log(constants.LOGLEVEL_VVV, "Skipping file at {0:#x}".format(mem_object.vol.offset))
|
|
continue
|
|
else:
|
|
name = renderers.NotApplicableValue()
|
|
|
|
yield (0, (constraint.type_name, format_hints.Hex(header.vol.offset), header.vol.layer_name, name))
|
|
|
|
@staticmethod
|
|
def builtin_constraints(symbol_table: str, tags: List[bytes]) -> List[PoolConstraint]:
|
|
"""Get built-in PoolConstraints given a list of pool tags"""
|
|
|
|
builtins = [
|
|
# atom tables
|
|
PoolConstraint(
|
|
b'AtmT',
|
|
type_name = symbol_table + constants.BANG + "_RTL_ATOM_TABLE",
|
|
size = (200, None),
|
|
page_type = PoolType.PAGED | PoolType.NONPAGED | PoolType.FREE),
|
|
# processes on windows before windows 8
|
|
PoolConstraint(
|
|
b'Pro\xe3',
|
|
type_name = symbol_table + constants.BANG + "_EPROCESS",
|
|
object_type = "Process",
|
|
size = (600, None),
|
|
page_type = PoolType.PAGED | PoolType.NONPAGED | PoolType.FREE),
|
|
# processes on windows starting with windows 8
|
|
PoolConstraint(
|
|
b'Proc',
|
|
type_name = symbol_table + constants.BANG + "_EPROCESS",
|
|
object_type = "Process",
|
|
size = (600, None),
|
|
page_type = PoolType.PAGED | PoolType.NONPAGED | PoolType.FREE),
|
|
# files on windows before windows 8
|
|
PoolConstraint(
|
|
b'Fil\xe5',
|
|
type_name = symbol_table + constants.BANG + "_FILE_OBJECT",
|
|
object_type = "File",
|
|
size = (150, None),
|
|
page_type = PoolType.PAGED | PoolType.NONPAGED | PoolType.FREE),
|
|
# files on windows starting with windows 8
|
|
PoolConstraint(
|
|
b'File',
|
|
type_name = symbol_table + constants.BANG + "_FILE_OBJECT",
|
|
object_type = "File",
|
|
size = (150, None),
|
|
page_type = PoolType.PAGED | PoolType.NONPAGED | PoolType.FREE),
|
|
]
|
|
|
|
return [constraint for constraint in builtins if constraint.tag in tags]
|
|
|
|
@classmethod
|
|
def generate_pool_scan(cls,
|
|
context: interfaces.context.ContextInterface,
|
|
layer_name: str,
|
|
symbol_table: str,
|
|
constraints: List[PoolConstraint]) \
|
|
-> Generator[Tuple[PoolConstraint, interfaces.objects.ObjectInterface, interfaces.objects.ObjectInterface], None, None]:
|
|
|
|
# get the object type map
|
|
type_map = handles.Handles.list_objects(
|
|
context = context, layer_name = layer_name, symbol_table = symbol_table)
|
|
|
|
cookie = handles.Handles.find_cookie(
|
|
context = context, layer_name = layer_name, symbol_table = symbol_table)
|
|
|
|
is_windows_10 = cls.is_windows_10(context = context, symbol_table = symbol_table)
|
|
is_windows_8_or_later = cls.is_windows_8_or_later(
|
|
context = context, layer_name = layer_name, symbol_table = symbol_table)
|
|
|
|
# start off with the primary virtual layer
|
|
scan_layer = layer_name
|
|
|
|
# switch to a non-virtual layer if necessary
|
|
if not is_windows_10:
|
|
scan_layer = context.memory[scan_layer].config['memory_layer']
|
|
|
|
for constraint, header in cls.pool_scan(
|
|
context, scan_layer, symbol_table, constraints, alignment = 8):
|
|
|
|
mem_object = header.get_object(
|
|
type_name = constraint.type_name,
|
|
type_map = type_map,
|
|
use_top_down = is_windows_8_or_later,
|
|
object_type = constraint.object_type,
|
|
native_layer_name = 'primary',
|
|
cookie = cookie)
|
|
|
|
if mem_object is None:
|
|
vollog.log(constants.LOGLEVEL_VVV, "Cannot create an instance of {}".format(constraint.type_name))
|
|
continue
|
|
|
|
yield constraint, mem_object, header
|
|
|
|
@classmethod
|
|
def pool_scan(cls,
|
|
context: interfaces.context.ContextInterface,
|
|
layer_name: str,
|
|
symbol_table: str,
|
|
pool_constraints: List[PoolConstraint],
|
|
alignment: int = 8,
|
|
progress_callback: Optional[constants.ProgressCallback] = None) \
|
|
-> Generator[Tuple[PoolConstraint, interfaces.objects.ObjectInterface], None, None]:
|
|
"""Returns the _POOL_HEADER object (based on the symbol_table template) after scanning through layer_name
|
|
returning all headers that match any of the constraints provided. Only one constraint can be provided per tag"""
|
|
# Setup the pattern
|
|
constraint_lookup = {} # type: Dict[bytes, List[PoolConstraint]]
|
|
for constraint in pool_constraints:
|
|
temp_list = constraint_lookup.get(constraint.tag, [])
|
|
temp_list.append(constraint)
|
|
constraint_lookup[constraint.tag] = temp_list
|
|
# Setup the pool header and offset differential
|
|
try:
|
|
module = context.module(symbol_table, layer_name, offset = 0)
|
|
header_type = module.get_type('_POOL_HEADER')
|
|
except exceptions.SymbolError:
|
|
# We have to manually load a symbol table
|
|
|
|
if symbols.symbol_table_is_64bit(context, symbol_table):
|
|
is_win_7 = PoolScanner.is_windows_7(context, 'primary', symbol_table)
|
|
if is_win_7:
|
|
pool_header_json_filename = "poolheader-x64-win7"
|
|
else:
|
|
pool_header_json_filename = "poolheader-x64"
|
|
else:
|
|
pool_header_json_filename = "poolheader-x86"
|
|
|
|
new_table_name = PoolHeaderSymbolTable.create(
|
|
context = context,
|
|
config_path = configuration.path_join(context.symbol_space[symbol_table].config_path, "poolheader"),
|
|
sub_path = "windows",
|
|
filename = pool_header_json_filename,
|
|
table_mapping = {'nt_symbols': symbol_table})
|
|
module = context.module(new_table_name, layer_name, offset = 0)
|
|
header_type = module.get_type('_POOL_HEADER')
|
|
|
|
header_offset = header_type.relative_child_offset('PoolTag')
|
|
|
|
# Run the scan locating the offsets of a particular tag
|
|
layer = context.memory[layer_name]
|
|
scanner = scanners.MultiStringScanner([c for c in constraint_lookup.keys()])
|
|
for offset, pattern in layer.scan(context, scanner, progress_callback = progress_callback):
|
|
for constraint in constraint_lookup[pattern]:
|
|
header = module.object(type_name = "_POOL_HEADER", offset = offset - header_offset)
|
|
|
|
# Size check
|
|
try:
|
|
if constraint.size is not None:
|
|
if constraint.size[0]:
|
|
if (alignment * header.BlockSize) < constraint.size[0]:
|
|
continue
|
|
if constraint.size[1]:
|
|
if (alignment * header.BlockSize) > constraint.size[1]:
|
|
continue
|
|
|
|
# Type check
|
|
if constraint.page_type is not None:
|
|
checks_pass = False
|
|
|
|
if (constraint.page_type & PoolType.FREE) and header.PoolType == 0:
|
|
checks_pass = True
|
|
elif (constraint.page_type &
|
|
PoolType.PAGED) and header.PoolType % 2 == 0 and header.PoolType > 0:
|
|
checks_pass = True
|
|
elif (constraint.page_type & PoolType.NONPAGED) and header.PoolType % 2 == 1:
|
|
checks_pass = True
|
|
|
|
if not checks_pass:
|
|
continue
|
|
|
|
if constraint.index is not None:
|
|
if constraint.index[0]:
|
|
if header.index < constraint.index[0]:
|
|
continue
|
|
if constraint.index[1]:
|
|
if header.index > constraint.index[1]:
|
|
continue
|
|
except exceptions.InvalidAddressException:
|
|
# The tested object's header doesn't point to valid addresses, ignore it
|
|
continue
|
|
|
|
# We found one that passed!
|
|
yield (constraint, header)
|
|
|
|
def run(self) -> renderers.TreeGrid:
|
|
return renderers.TreeGrid([("Tag", str), ("Offset", format_hints.Hex), ("Layer", str), ("Name", str)],
|
|
self._generator())
|