Source code for volatility3.plugins.windows.shimcachemem

# This file is Copyright 2020 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
import os
from datetime import datetime
from itertools import count
from typing import Iterator, List, Optional, Tuple

from volatility3.framework import constants, exceptions, interfaces, renderers, symbols
from volatility3.framework.configuration import requirements
from volatility3.framework.objects.utility import array_to_string
from volatility3.framework.renderers import format_hints
from volatility3.framework.symbols import intermed
from volatility3.framework.symbols.windows import versions
from volatility3.framework.symbols.windows.extensions import pe, shimcache
from volatility3.plugins import timeliner
from volatility3.plugins.windows import modules, pslist, vadinfo

vollog = logging.getLogger(__name__)


[docs] class ShimcacheMem(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface): """Reads Shimcache entries from the ahcache.sys AVL tree""" _required_framework_version = (2, 0, 0) _version = (1, 0, 1) # These checks must be completed from newest -> oldest OS version. _win_version_file_map: List[Tuple[versions.OsDistinguisher, bool, str]] = [ (versions.is_win10, True, "shimcache-win10-x64"), (versions.is_win10, False, "shimcache-win10-x86"), (versions.is_windows_8_or_later, True, "shimcache-win8-x64"), (versions.is_windows_8_or_later, False, "shimcache-win8-x86"), (versions.is_windows_7, True, "shimcache-win7-x64"), (versions.is_windows_7, False, "shimcache-win7-x86"), (versions.is_vista_or_later, True, "shimcache-vista-x64"), (versions.is_vista_or_later, False, "shimcache-vista-x86"), (versions.is_2003, False, "shimcache-2003-x86"), (versions.is_2003, True, "shimcache-2003-x64"), (versions.is_windows_xp_sp3, False, "shimcache-xp-sp3-x86"), (versions.is_windows_xp_sp2, False, "shimcache-xp-sp2-x86"), (versions.is_xp_or_2003, True, "shimcache-xp-2003-x64"), (versions.is_xp_or_2003, False, "shimcache-xp-2003-x86"), ] NT_KRNL_MODS = ["ntoskrnl.exe", "ntkrnlpa.exe", "ntkrnlmp.exe", "ntkrpamp.exe"]
[docs] def generate_timeline( self, ) -> 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, ) if isinstance(last_modified, datetime): yield ( f"Shimcache: File {file_path} modified", timeliner.TimeLinerType.MODIFIED, last_modified, )
[docs] @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="pslist", component=pslist.PsList, version=(3, 0, 0) ), requirements.VersionRequirement( name="timeliner", component=timeliner.TimeLinerInterface, version=(1, 0, 0), ), requirements.VersionRequirement( name="vadinfo", component=vadinfo.VadInfo, version=(2, 0, 0) ), requirements.VersionRequirement( name="modules", component=modules.Modules, version=(3, 0, 0) ), ]
[docs] @classmethod def create_shimcache_table( cls, context: interfaces.context.ContextInterface, symbol_table_name: str, config_path: str, ) -> str: """Creates a shimcache symbol table Args: context: The context to retrieve required elements (layers, symbol tables) from symbol_table: The name of an existing symbol table containing the kernel symbols config_path: The configuration path within the context of the symbol table to create Returns: The name of the constructed shimcache table """ native_types = context.symbol_space[symbol_table_name].natives is_64bit = symbols.symbol_table_is_64bit( context=context, symbol_table_name=symbol_table_name ) table_mapping = {"nt_symbols": symbol_table_name} try: symbol_filename = next( filename for version_check, for_64bit, filename in ShimcacheMem._win_version_file_map if is_64bit == for_64bit and version_check(context=context, symbol_table=symbol_table_name) ) except StopIteration: raise NotImplementedError("This version of Windows is not supported!") vollog.debug(f"Using shimcache table {symbol_filename}") return intermed.IntermediateSymbolTable.create( context, config_path, os.path.join("windows", "shimcache"), symbol_filename, class_types=shimcache.class_types, native_types=native_types, table_mapping=table_mapping, )
[docs] @classmethod def find_shimcache_win_xp( cls, context: interfaces.context.ContextInterface, kernel_module_name: str, shimcache_symbol_table: str, ) -> Iterator[shimcache.SHIM_CACHE_ENTRY]: """Attempts to find the shimcache in a Windows XP memory image :param context: The context to retrieve required elements (layers, symbol tables) from :param layer_name: The name of the memory layer on which to operate. :param kernel_symbol_table: The name of an existing symbol table containing the kernel symbols :param shimcache_symbol_table: The name of a symbol table containing the hand-crafted shimcache symbols """ SHIM_NUM_ENTRIES_OFFSET = 0x8 SHIM_MAX_ENTRIES = 0x60 # 96 max entries in XP shim cache SHIM_LRU_OFFSET = 0x10 SHIM_HEADER_SIZE = 0x190 SHIM_CACHE_ENTRY_SIZE = 0x228 seen = set() for process in pslist.PsList.list_processes(context, kernel_module_name): pid = process.UniqueProcessId vollog.debug("checking process %d", pid) for vad in vadinfo.VadInfo.list_vads( process, lambda x: x.get_tag() == b"Vad " and x.Protection == 4 ): try: proc_layer_name = process.add_process_layer() proc_layer = context.layers[proc_layer_name] except exceptions.InvalidAddressException: continue try: 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 except exceptions.InvalidAddressException: continue num_entries = context.object( shimcache_symbol_table + constants.BANG + "unsigned int", proc_layer_name, vad.get_start() + SHIM_NUM_ENTRIES_OFFSET, ) vollog.debug(f"Found {num_entries} shimcache entries") if num_entries > SHIM_MAX_ENTRIES: continue cache_idx_ptr = vad.get_start() + SHIM_LRU_OFFSET for _ in range(num_entries): cache_idx_val = proc_layer.context.object( shimcache_symbol_table + constants.BANG + "unsigned long", proc_layer_name, cache_idx_ptr, ) cache_idx_ptr += 4 if cache_idx_val > SHIM_MAX_ENTRIES - 1: continue shim_entry_offset = ( vad.get_start() + SHIM_HEADER_SIZE + (SHIM_CACHE_ENTRY_SIZE * cache_idx_val) ) if not proc_layer.is_valid(shim_entry_offset): continue physical_addr = proc_layer.translate(shim_entry_offset) if physical_addr in seen: continue shim_entry = proc_layer.context.object( shimcache_symbol_table + constants.BANG + "SHIM_CACHE_ENTRY", proc_layer_name, shim_entry_offset, ) if not proc_layer.is_valid(shim_entry.vol.offset): continue if not shim_entry.is_valid(): continue seen.add(physical_addr) yield shim_entry
[docs] @classmethod def find_shimcache_win_2k3_to_7( cls, context: interfaces.context.ContextInterface, config_path: str, kernel_module_name: str, shimcache_symbol_table: str, ) -> Iterator[shimcache.SHIM_CACHE_ENTRY]: """Implements the algorithm to search for the shim cache on Windows 2000 (x64) through Windows 7 / 2008 R2. The algorithm consists of the following: 1) Find the NT kernel module's .data and PAGE sections 2) Iterate over every 4/8 bytes (depending on OS bitness) in the .data section and test for the following: a) offset represents a valid RTL_AVL_TABLE object b) RTL_AVL_TABLE is preceded by an ERESOURCE object c) RTL_AVL_TABLE is followed by the beginning of the SHIM LRU list :param context: The context to retrieve required elements (layers, symbol tables) from :param layer_name: The name of the memory layer on which to operate. :param kernel_symbol_table: The name of an existing symbol table containing the kernel symbols :param shimcache_symbol_table: The name of a symbol table containing the hand-crafted shimcache symbols """ kernel = context.modules[kernel_module_name] data_sec = cls.get_module_section_range( context, config_path, kernel_module_name, cls.NT_KRNL_MODS, ".data", ) mod_page = cls.get_module_section_range( context, config_path, kernel_module_name, cls.NT_KRNL_MODS, "PAGE", ) # We require both in order to accurately handle AVL table if not (data_sec and mod_page): return data_sec_offset, data_sec_size = data_sec mod_page_offset, mod_page_size = mod_page addr_size = ( 8 if symbols.symbol_table_is_64bit( context=context, symbol_table_name=kernel.symbol_table_name ) else 4 ) shim_head = None for offset in range( data_sec_offset, data_sec_offset + data_sec_size, addr_size ): shim_head = cls.try_get_shim_head_at_offset( context, shimcache_symbol_table, kernel_module_name, mod_page_offset, mod_page_offset + mod_page_size, offset, ) if shim_head: break if not shim_head: return yield from shim_head.ListEntry.to_list( shimcache_symbol_table + constants.BANG + "SHIM_CACHE_ENTRY", "ListEntry" )
[docs] @classmethod def try_get_shim_head_at_offset( cls, context: interfaces.context.ContextInterface, shimcache_symbol_table: str, kernel_module_name: str, mod_page_start: int, mod_page_end: int, offset: int, ) -> Optional[shimcache.SHIM_CACHE_ENTRY]: """Attempts to construct a SHIM_CACHE_HEAD within a layer of the given context, using the provided offset within that layer, as well as the start and end offsets of the kernel module's `PAGE` section start and end offsets. If a number of validity checks are passed, this method will return the `SHIM_CACHE_HEAD` object. Otherwise, `None` is returned. """ kernel = context.modules[kernel_module_name] # Check RTL_AVL_TABLE at offset rtl_avl_table = context.object( shimcache_symbol_table + constants.BANG + "_RTL_AVL_TABLE", kernel.layer_name, offset, ) if not rtl_avl_table.is_valid(mod_page_start, mod_page_end): return None vollog.debug(f"Candidate RTL_AVL_TABLE found at offset {offset:#x}") ersrc_size = context.symbol_space.get_type( kernel.symbol_table_name + constants.BANG + "_ERESOURCE" ).size ersrc_alignment = ( 0x20 if symbols.symbol_table_is_64bit( context=context, symbol_table_name=kernel.symbol_table_name ) else 0x10 # 0x20 if context.symbol_space.get_type("pointer").size == 8 else 0x10 ) vollog.debug( f"ERESOURCE size: {ersrc_size:#x}, ERESOURCE alignment: {ersrc_alignment:#x}" ) eresource_rel_off = ersrc_size + ((offset - ersrc_size) % ersrc_alignment) eresource_offset = offset - eresource_rel_off vollog.debug(f"Constructing ERESOURCE at {eresource_offset:#x}") eresource = context.object( kernel.symbol_table_name + constants.BANG + "_ERESOURCE", kernel.layer_name, eresource_offset, ) if not eresource.is_valid(): vollog.debug("ERESOURCE Invalid") return None shim_head_offset = offset + rtl_avl_table.vol.size if not context.layers[kernel.layer_name].is_valid(shim_head_offset): return None shim_head = context.object( shimcache_symbol_table + constants.BANG + "SHIM_CACHE_ENTRY", kernel.layer_name, shim_head_offset, ) if not shim_head.is_valid(): vollog.debug("shim head invalid") return None else: vollog.debug("returning shim head") return shim_head
[docs] @classmethod def find_shimcache_win_8_or_later( cls, context: interfaces.context.ContextInterface, config_path: str, kernel_module_name: str, shimcache_symbol_table: str, ) -> Iterator[shimcache.SHIM_CACHE_ENTRY]: """Attempts to locate and yield shimcache entries from a Windows 8 or later memory image. :param context: The context to retrieve required elements (layers, symbol tables) from :param layer_name: The name of the memory layer on which to operate. :param kernel_symbol_table: The name of an existing symbol table containing the kernel symbols :param shimcache_symbol_table: The name of a symbol table containing the hand-crafted shimcache symbols """ kernel = context.modules[kernel_module_name] is_8_1_or_later = versions.is_windows_8_1_or_later( context, kernel.symbol_table_name ) or versions.is_win10(context, kernel.symbol_table_name) module_names = ["ahcache.sys"] if is_8_1_or_later else cls.NT_KRNL_MODS vollog.debug(f"Searching for modules {module_names}") data_sec = cls.get_module_section_range( context, config_path, kernel_module_name, module_names, ".data", ) mod_page = cls.get_module_section_range( context, config_path, kernel_module_name, module_names, "PAGE", ) if not (data_sec and mod_page): return None mod_page_offset, mod_page_size = mod_page data_sec_offset, data_sec_size = data_sec # iterate over ahcache kernel module's .data section in search of *two* SHIM handles shim_heads = [] vollog.debug(f"PAGE offset: {mod_page_offset:#x}") vollog.debug(f".data offset: {data_sec_offset:#x}") handle_type = context.symbol_space.get_type( shimcache_symbol_table + constants.BANG + "SHIM_CACHE_HANDLE" ) for offset in range( data_sec_offset, data_sec_offset + data_sec_size, ( 8 if symbols.symbol_table_is_64bit( context=context, symbol_table_name=kernel.symbol_table_name ) else 4 ), ): vollog.debug(f"Building shim handle pointer at {offset:#x}") shim_handle = context.object( object_type=shimcache_symbol_table + constants.BANG + "pointer", layer_name=kernel.layer_name, subtype=handle_type, offset=offset, ) if shim_handle.is_valid(mod_page_offset, mod_page_offset + mod_page_size): if shim_handle.head is not None: vollog.debug( f"Found valid shim handle @ {shim_handle.vol.offset:#x}" ) shim_heads.append(shim_handle.head) if len(shim_heads) == 2: break if len(shim_heads) != 2: vollog.debug("Failed to identify two valid SHIM_CACHE_HANDLE structures") return # On Windows 8 x64, the first cache contains the shim cache. # On Windows 8 x86, 8.1 x86/x64, and 10, the second cache contains the shim cache. if ( not symbols.symbol_table_is_64bit( context=context, symbol_table_name=kernel.symbol_table_name ) and not is_8_1_or_later ): valid_head = shim_heads[1] elif not is_8_1_or_later: valid_head = shim_heads[0] else: valid_head = shim_heads[1] for shim_entry in valid_head.ListEntry.to_list( shimcache_symbol_table + constants.BANG + "SHIM_CACHE_ENTRY", "ListEntry" ): if shim_entry.is_valid(): yield shim_entry
def _generator(self): kernel = self.context.modules[self.config["kernel"]] shimcache_table_name = self.create_shimcache_table( self.context, kernel.symbol_table_name, self.config_path ) c = count() if versions.is_windows_8_or_later(self._context, kernel.symbol_table_name): vollog.info("Finding shimcache entries for Windows 8.0+") entries = self.find_shimcache_win_8_or_later( self.context, self.config_path, self.config["kernel"], shimcache_table_name, ) elif ( versions.is_2003(self.context, kernel.symbol_table_name) or versions.is_vista_or_later(self.context, kernel.symbol_table_name) or versions.is_windows_7(self.context, kernel.symbol_table_name) ): vollog.info("Finding shimcache entries for Windows 2k3/Vista/7") entries = self.find_shimcache_win_2k3_to_7( self.context, self.config_path, self.config["kernel"], shimcache_table_name, ) elif versions.is_windows_xp_sp2( self._context, kernel.symbol_table_name ) or versions.is_windows_xp_sp3(self.context, kernel.symbol_table_name): vollog.info("Finding shimcache entries for WinXP") entries = self.find_shimcache_win_xp( self._context, self.config["kernel"], shimcache_table_name, ) else: vollog.warn("Cannot parse shimcache entries for this version of Windows") return for entry in entries: try: vollog.debug(f"SHIM_CACHE_ENTRY type: {entry.__class__}") shim_entry = ( entry.last_modified, entry.last_update, entry.exec_flag, ( format_hints.Hex(entry.file_size) if isinstance(entry.file_size, int) else entry.file_size ), entry.file_path, ) except exceptions.InvalidAddressException: continue yield ( 0, (next(c), *shim_entry), )
[docs] def run(self): return renderers.TreeGrid( [ ("Order", int), ("Last Modified", datetime), ("Last Update", datetime), ("Exec Flag", bool), ("File Size", format_hints.Hex), ("File Path", str), ], self._generator(), )
[docs] @classmethod def get_module_section_range( cls, context: interfaces.context.ContextInterface, config_path: str, kernel_module_name: str, module_list: List[str], section_name: str, ) -> Optional[Tuple[int, int]]: """Locates the size and offset of the first found module section specified by name from the list of modules. :param context: The context to operate on :param layer_name: The memory layer to read from :param module_list: A list of module names to search for the given section :param section_name: The name of the section to search for. :return: The offset and size of the module, if found; Otherwise, returns `None` """ krnl_mod = None for module in modules.Modules.list_modules(context, kernel_module_name): try: if module.BaseDllName.String in module_list: krnl_mod = module break except exceptions.InvalidAddressException as exc: vollog.warning( f"Failed to get kernel module due to {exc.__class__.__name__}: {exc.invalid_address:#x}" ) if krnl_mod is None: vollog.warning("Failed to find kernel module") return None kernel = context.modules[kernel_module_name] pe_table_name = intermed.IntermediateSymbolTable.create( context, interfaces.configuration.path_join(config_path, "pe"), "windows", "pe", class_types=pe.class_types, ) # code taken from Win32KBase._section_chunks (win32_core.py) dos_header = context.object( pe_table_name + constants.BANG + "_IMAGE_DOS_HEADER", kernel.layer_name, offset=krnl_mod.DllBase, ) if not dos_header: return None nt_header = dos_header.get_nt_header() try: section = next( sec for sec in nt_header.get_sections() if section_name.lower() == array_to_string(sec.Name).lower() ) except StopIteration: return None section_offset = krnl_mod.DllBase + section.VirtualAddress section_size = section.Misc.VirtualSize return section_offset, section_size