"""Application Compatibility Cache collector."""
import logging
from dtfabric.runtime import data_maps as dtfabric_data_maps
from winregrc import data_format
from winregrc import errors
from winregrc import interface
[docs]
class AppCompatCacheCachedEntry:
"""Application Compatibility Cache cached entry.
Attributes:
cached_entry_size (int): size of the cached entry.
data (bytes): data of the cached entry.
file_size (int): size of file corresponding to the cached entry.
insertion_flags (int): insertion flags of the cached entry.
last_modification_time (int): last modification timestamp of the file
corresponding to the cached entry.
last_update_time (int): last update timestamp the cached entry.
shim_flags (int): shim flags of the cached entry.
path (str): path of the cached entry.
"""
[docs]
def __init__(self):
"""Initializes an Application Compatibility Cache cached entry."""
super().__init__()
self.cached_entry_size = 0
self.data = None
self.file_size = None
self.insertion_flags = None
self.last_modification_time = None
self.last_update_time = None
self.shim_flags = None
self.path = None
[docs]
class AppCompatCacheDataParser(data_format.BinaryDataFormat):
"""Application Compatibility Cache data parser."""
_DEFINITION_FILE = "appcompatcache.yaml"
_FORMAT_TYPE_2000 = 1
_FORMAT_TYPE_XP = 2
_FORMAT_TYPE_2003 = 3
_FORMAT_TYPE_VISTA = 4
_FORMAT_TYPE_7 = 5
_FORMAT_TYPE_8 = 6
_FORMAT_TYPE_10 = 7
_HEADER_SIGNATURES = {
# AppCompatCache format signature used in Windows XP.
0xDEADBEEF: _FORMAT_TYPE_XP,
# AppCompatCache format signature used in Windows 2003, Vista and 2008.
0xBADC0FFE: _FORMAT_TYPE_2003,
# AppCompatCache format signature used in Windows 7 and 2008 R2.
0xBADC0FEE: _FORMAT_TYPE_7,
# AppCompatCache format used in Windows 8.0 and 8.1.
0x00000080: _FORMAT_TYPE_8,
# AppCompatCache format used in Windows 10
0x00000030: _FORMAT_TYPE_10,
0x00000034: _FORMAT_TYPE_10,
}
_HEADER_DATA_TYPE_MAP_NAMES = {
_FORMAT_TYPE_XP: "appcompatcache_header_xp_32bit",
_FORMAT_TYPE_2003: "appcompatcache_header_2003",
_FORMAT_TYPE_VISTA: "appcompatcache_header_vista",
_FORMAT_TYPE_7: "appcompatcache_header_7",
_FORMAT_TYPE_8: "appcompatcache_header_8",
_FORMAT_TYPE_10: "appcompatcache_header_10",
}
_SUPPORTED_FORMAT_TYPES = frozenset(_HEADER_DATA_TYPE_MAP_NAMES.keys())
# AppCompatCache format used in Windows 8.0.
_CACHED_ENTRY_SIGNATURE_8_0 = b"00ts"
# AppCompatCache format used in Windows 8.1.
_CACHED_ENTRY_SIGNATURE_8_1 = b"10ts"
[docs]
def __init__(self, debug=False, output_writer=None):
"""Initializes an Application Compatibility Cache data parser.
Args:
debug (Optional[bool]): True if debug information should be written.
output_writer (Optional[OutputWriter]): output writer.
"""
super().__init__(debug=debug, output_writer=output_writer)
self._cached_entry_data_type_map = None
def _DebugPrintCachedEntryXP(self, cached_entry):
"""Prints Windows XP AppCompatCache cached entry value debug information.
Args:
cached_entry (appcompatcache_cached_entry_xp_32bit): Windows XP
AppCompatCache cached entry.
"""
# TODO: have dtFabric handle string conversion.
string_size = 0
for string_index in range(0, 528, 2):
if (
cached_entry.path[string_index] == 0
and cached_entry.path[string_index + 1] == 0
):
break
string_size += 2
path = bytearray(cached_entry.path[0:string_size]).decode("utf-16-le")
self._DebugPrintValue("Path", path)
self._DebugPrintDecimalValue("File size", cached_entry.file_size)
self._DebugPrintFiletimeValue("Last update time", cached_entry.last_update_time)
def _DebugPrintCachedEntry2003(self, cached_entry):
"""Prints Windows 2003 AppCompatCache cached entry value debug information.
Args:
cached_entry_common (appcompatcache_cached_entry_2003_32bit|
appcompatcache_cached_entry_2003_64bit|
appcompatcache_cached_entry_vista_32bit|
appcompatcache_cached_entry_vista_64bit|
appcompatcache_cached_entry_7_32bit|
appcompatcache_cached_entry_7_64bit): Windows 2003,
Vista or 7 AppCompatCache cached entry.
"""
self._DebugPrintDecimalValue("Path size", cached_entry.path_size)
self._DebugPrintDecimalValue(
"Maximum path size", cached_entry.maximum_path_size
)
self._DebugPrintValue("Path offset", f"0x{cached_entry.path_offset:08x}")
if hasattr(cached_entry, "file_size"):
self._DebugPrintDecimalValue("File size", cached_entry.file_size)
if hasattr(cached_entry, "insertion_flags"):
self._DebugPrintValue(
"Insertion flags", f"0x{cached_entry.insertion_flags:08x}"
)
if hasattr(cached_entry, "shim_flags"):
self._DebugPrintValue("Shim flags", f"0x{cached_entry.shim_flags:08x}")
if hasattr(cached_entry, "data_offset"):
self._DebugPrintValue("Data offset", f"0x{cached_entry.data_offset:08x}")
if hasattr(cached_entry, "data_size"):
self._DebugPrintDecimalValue("Data size", cached_entry.data_size)
# pylint: disable=missing-type-doc
def _DebugPrintCachedEntry8(self, cached_entry_header, cached_entry_body):
"""Prints Windows 8 AppCompatCache cached entry value debug information.
Args:
cached_entry_header (appcompatcache_cached_entry_header_8): Windows 8 or
10 AppCompatCache cached entry header.
cached_entry_body (appcompatcache_cached_entry_header_8_0|
appcompatcache_cached_entry_header_8_1|
appcompatcache_cached_entry_header_10): Windows 8.0, 8.1 or 10
AppCompatCache cached entry body.
"""
self._DebugPrintValue(
"Signature", cached_entry_header.signature.decode("ascii")
)
self._DebugPrintValue("Unknown1", f"0x{cached_entry_header.unknown1:08x}")
self._DebugPrintDecimalValue(
"Cached entry data size", cached_entry_header.cached_entry_data_size
)
self._DebugPrintDecimalValue("Path size", cached_entry_body.path_size)
self._DebugPrintValue("Path", cached_entry_body.path.rstrip("\x00"))
if hasattr(cached_entry_body, "insertion_flags"):
self._DebugPrintValue(
"Insertion flags", f"0x{cached_entry_body.insertion_flags:08x}"
)
if hasattr(cached_entry_body, "shim_flags"):
self._DebugPrintValue("Shim flags", f"0x{cached_entry_body.shim_flags:08x}")
if hasattr(cached_entry_body, "unknown1"):
self._DebugPrintValue("Unknown1", f"0x{cached_entry_body.unknown1:04x}")
self._DebugPrintFiletimeValue(
"Last modification time", cached_entry_body.last_modification_time
)
self._DebugPrintDecimalValue("Data size", cached_entry_body.data_size)
# pylint: disable=missing-type-doc
def _DebugPrintHeader(self, format_type, header):
"""Prints AppCompatCache header value debug information.
Args:
format_type (int): AppCompatCache format type.
header (appcompatcache_header_xp_32bit|appcompatcache_header_vista|
appcompatcache_header_7|appcompatcache_header_8|
appcompatcache_header_10): AppCompatCache header.
"""
if format_type == self._FORMAT_TYPE_10:
self._DebugPrintDecimalValue("Header size", header.signature)
else:
self._DebugPrintValue("Signature", f"0x{header.signature:08x}")
if format_type in (
self._FORMAT_TYPE_XP,
self._FORMAT_TYPE_2003,
self._FORMAT_TYPE_VISTA,
self._FORMAT_TYPE_7,
self._FORMAT_TYPE_10,
):
self._DebugPrintDecimalValue(
"Number of cached entries", header.number_of_cached_entries
)
if format_type == self._FORMAT_TYPE_XP:
if self._debug:
self._DebugPrintValue(
"Number of LRU entries", f"0x{header.number_of_lru_entries:08x}"
)
self._DebugPrintValue("Unknown1", f"0x{header.unknown1:08x}")
elif format_type == self._FORMAT_TYPE_8:
self._DebugPrintValue("Unknown1", f"0x{header.unknown1:08x}")
if format_type != self._FORMAT_TYPE_XP:
self._DebugPrintText("\n")
def _GetCachedEntryDataTypeMap(self, format_type, value_data, cached_entry_offset):
"""Determines the cached entry data type map.
Args:
format_type (int): format type.
value_data (bytes): value data.
cached_entry_offset (int): offset of the first cached entry data
relative to the start of the value data.
Returns:
dtfabric.DataTypeMap: data type map which contains a data type definition,
such as a structure, that can be mapped onto binary data or None
if the data type map is not defined.
Raises:
ParseError: if the cached entry data type map cannot be determined.
"""
if format_type not in self._SUPPORTED_FORMAT_TYPES:
raise errors.ParseError(f"Unsupported format type: {format_type:d}")
data_type_map_name = ""
if format_type == self._FORMAT_TYPE_XP:
data_type_map_name = "appcompatcache_cached_entry_xp_32bit"
elif format_type in (self._FORMAT_TYPE_8, self._FORMAT_TYPE_10):
data_type_map_name = "appcompatcache_cached_entry_header_8"
else:
cached_entry = self._ParseCommon2003CachedEntry(
value_data, cached_entry_offset
)
# Assume the entry is 64-bit if the 32-bit path offset is 0 and
# the 64-bit path offset is set.
if (
cached_entry.path_offset_32bit == 0
and cached_entry.path_offset_64bit != 0
):
number_of_bits = "64"
else:
number_of_bits = "32"
if format_type == self._FORMAT_TYPE_2003:
data_type_map_name = (
f"appcompatcache_cached_entry_2003_{number_of_bits:s}bit"
)
elif format_type == self._FORMAT_TYPE_VISTA:
data_type_map_name = (
f"appcompatcache_cached_entry_vista_{number_of_bits:s}bit"
)
elif format_type == self._FORMAT_TYPE_7:
data_type_map_name = (
f"appcompatcache_cached_entry_7_{number_of_bits:s}bit"
)
return self._GetDataTypeMap(data_type_map_name)
def _ParseCommon2003CachedEntry(self, value_data, cached_entry_offset):
"""Parses the cached entry structure common for Windows 2003, Vista and 7.
Args:
value_data (bytes): value data.
cached_entry_offset (int): offset of the first cached entry data
relative to the start of the value data.
Returns:
appcompatcache_cached_entry_2003_common: cached entry structure common
for Windows 2003, Windows Vista and Windows 7.
Raises:
ParseError: if the value data could not be parsed.
"""
cached_entry_data = value_data[cached_entry_offset:]
data_type_map = self._GetDataTypeMap("appcompatcache_cached_entry_2003_common")
try:
cached_entry = self._ReadStructureFromByteStream(
cached_entry_data, cached_entry_offset, data_type_map, "cached entry"
)
except (ValueError, errors.ParseError) as exception:
raise errors.ParseError(
f"Unable to parse cached entry value with error: {exception!s}"
)
if cached_entry.path_size > cached_entry.maximum_path_size:
raise errors.ParseError("Path size value out of bounds.")
path_end_of_string_size = (
cached_entry.maximum_path_size - cached_entry.path_size
)
if cached_entry.path_size == 0 or path_end_of_string_size != 2:
raise errors.ParseError("Unsupported path size values.")
return cached_entry
[docs]
def CheckSignature(self, value_data):
"""Parses and validates the signature.
Args:
value_data (bytes): value data.
Returns:
int: format type or None if format could not be determined.
Raises:
ParseError: if the value data could not be parsed.
"""
data_type_map = self._GetDataTypeMap("uint32le")
try:
signature = self._ReadStructureFromByteStream(
value_data, 0, data_type_map, "signature"
)
except (ValueError, errors.ParseError) as exception:
raise errors.ParseError(
f"Unable to parse signature value with error: {exception!s}"
)
format_type = self._HEADER_SIGNATURES.get(signature)
if format_type == self._FORMAT_TYPE_2003:
# TODO: determine which format version is used (2003 or Vista).
return self._FORMAT_TYPE_2003
if format_type == self._FORMAT_TYPE_8:
cached_entry_signature = value_data[signature : signature + 4]
if cached_entry_signature in (
self._CACHED_ENTRY_SIGNATURE_8_0,
self._CACHED_ENTRY_SIGNATURE_8_1,
):
return self._FORMAT_TYPE_8
elif format_type == self._FORMAT_TYPE_10:
# Windows 10 uses the same cache entry signature as Windows 8.1
cached_entry_signature = value_data[signature : signature + 4]
if cached_entry_signature == self._CACHED_ENTRY_SIGNATURE_8_1:
return self._FORMAT_TYPE_10
return format_type
[docs]
def ParseCachedEntry(
self, format_type, value_data, cached_entry_index, cached_entry_offset
):
"""Parses a cached entry.
Args:
format_type (int): format type.
value_data (bytes): value data.
cached_entry_index (int): cached entry index.
cached_entry_offset (int): offset of the first cached entry data
relative to the start of the value data.
Returns:
AppCompatCacheCachedEntry: cached entry.
Raises:
ParseError: if the value data could not be parsed.
"""
if not self._cached_entry_data_type_map:
self._cached_entry_data_type_map = self._GetCachedEntryDataTypeMap(
format_type, value_data, cached_entry_offset
)
if not self._cached_entry_data_type_map:
raise errors.ParseError("Unable to determine cached entry data type.")
cached_entry_size = self._cached_entry_data_type_map.GetSizeHint()
cached_entry_end_offset = cached_entry_offset + cached_entry_size
cached_entry_data = value_data[cached_entry_offset:cached_entry_end_offset]
if self._debug:
if format_type not in (self._FORMAT_TYPE_8, self._FORMAT_TYPE_10):
description = f"Cached entry: {cached_entry_index:d} data"
self._DebugPrintData(description, cached_entry_data)
try:
cached_entry = self._ReadStructureFromByteStream(
cached_entry_data,
cached_entry_offset,
self._cached_entry_data_type_map,
"cached entry",
)
except (ValueError, errors.ParseError) as exception:
if self._debug:
if format_type in (self._FORMAT_TYPE_8, self._FORMAT_TYPE_10):
description = f"Cached entry: {cached_entry_index:d} header data"
self._DebugPrintData(description, cached_entry_data)
raise errors.ParseError(
f"Unable to parse cached entry value with error: {exception!s}"
)
if format_type in (self._FORMAT_TYPE_8, self._FORMAT_TYPE_10):
if cached_entry.signature not in (
self._CACHED_ENTRY_SIGNATURE_8_0,
self._CACHED_ENTRY_SIGNATURE_8_1,
):
if self._debug:
description = f"Cached entry: {cached_entry_index:d} header data"
self._DebugPrintData(description, cached_entry_data)
raise errors.ParseError("Unsupported cache entry signature")
cached_entry_object = AppCompatCacheCachedEntry()
data_offset = 0
data_size = 0
if format_type == self._FORMAT_TYPE_XP:
if self._debug:
self._DebugPrintCachedEntryXP(cached_entry)
# TODO: have dtFabric handle string conversion.
string_size = 0
for string_index in range(0, 528, 2):
if (
cached_entry.path[string_index] == 0
and cached_entry.path[string_index + 1] == 0
):
break
string_size += 2
last_modification_time = cached_entry.last_modification_time
path = bytearray(cached_entry.path[0:string_size]).decode("utf-16-le")
cached_entry_object.last_update_time = cached_entry.last_update_time
elif format_type in (
self._FORMAT_TYPE_2003,
self._FORMAT_TYPE_VISTA,
self._FORMAT_TYPE_7,
):
if self._debug:
self._DebugPrintCachedEntry2003(cached_entry)
last_modification_time = cached_entry.last_modification_time
if format_type in (self._FORMAT_TYPE_VISTA, self._FORMAT_TYPE_7):
cached_entry_object.insertion_flags = cached_entry.insertion_flags
cached_entry_object.shim_flags = cached_entry.shim_flags
path_size = cached_entry.path_size
maximum_path_size = cached_entry.maximum_path_size
path_offset = cached_entry.path_offset
if path_offset > 0 and path_size > 0:
path_size += path_offset
maximum_path_size += path_offset
if self._debug:
self._DebugPrintData(
"Path data", value_data[path_offset:maximum_path_size]
)
path = value_data[path_offset:path_size].decode("utf-16-le")
if self._debug:
self._DebugPrintValue("Path", path)
if format_type == self._FORMAT_TYPE_7:
data_offset = cached_entry.data_offset
data_size = cached_entry.data_size
elif format_type in (self._FORMAT_TYPE_8, self._FORMAT_TYPE_10):
cached_entry_data_size = cached_entry.cached_entry_data_size
cached_entry_size = 12 + cached_entry_data_size
cached_entry_end_offset = cached_entry_offset + cached_entry_size
cached_entry_data = value_data[cached_entry_offset:cached_entry_end_offset]
if self._debug:
description = f"Cached entry: {cached_entry_index:d} data"
self._DebugPrintData(description, cached_entry_data)
if format_type == self._FORMAT_TYPE_10:
data_type_map_name = "appcompatcache_cached_entry_body_10"
elif cached_entry.signature == self._CACHED_ENTRY_SIGNATURE_8_0:
data_type_map_name = "appcompatcache_cached_entry_body_8_0"
elif cached_entry.signature == self._CACHED_ENTRY_SIGNATURE_8_1:
data_type_map_name = "appcompatcache_cached_entry_body_8_1"
else:
data_type_map_name = None
data_type_map = self._GetDataTypeMap(data_type_map_name)
context = dtfabric_data_maps.DataTypeMapContext()
try:
cached_entry_body = self._ReadStructureFromByteStream(
cached_entry_data[12:],
cached_entry_offset + 12,
data_type_map,
"cached entry body",
context=context,
)
except (ValueError, errors.ParseError) as exception:
raise errors.ParseError(
f"Unable to parse cached entry body with error: {exception!s}"
)
if self._debug:
self._DebugPrintCachedEntry8(cached_entry, cached_entry_body)
last_modification_time = cached_entry_body.last_modification_time
path = cached_entry_body.path
if format_type == self._FORMAT_TYPE_8:
cached_entry_object.insertion_flags = cached_entry_body.insertion_flags
cached_entry_object.shim_flags = cached_entry_body.shim_flags
data_offset = cached_entry_offset + context.byte_size
data_size = cached_entry_body.data_size
if self._debug:
self._DebugPrintText("\n")
cached_entry_object.cached_entry_size = cached_entry_size
cached_entry_object.file_size = getattr(cached_entry, "file_size", None)
cached_entry_object.last_modification_time = last_modification_time
cached_entry_object.path = path
if data_size > 0:
cached_entry_object.data = value_data[data_offset : data_offset + data_size]
if self._debug:
self._DebugPrintData("Data", cached_entry_object.data)
return cached_entry_object
[docs]
class AppCompatCacheCollector(interface.WindowsRegistryKeyCollector):
"""Application Compatibility Cache collector.
Attributes:
cached_entries (list[AppCompatCacheCachedEntry]): cached entries.
"""
[docs]
def __init__(self, debug=False, output_writer=None):
"""Initializes a Application Compatibility Cache collector.
Args:
debug (Optional[bool]): True if debug information should be printed.
output_writer (Optional[OutputWriter]): output writer.
"""
super().__init__(debug=debug)
self._parser = AppCompatCacheDataParser(
debug=self._debug, output_writer=output_writer
)
self._output_writer = output_writer
self.cached_entries = []
def _CollectAppCompatCacheFromKey(self, app_compat_cache_key):
"""Collects Application Compatibility Cache from a Windows Registry key.
Args:
app_compat_cache_key (dfwinreg.WinRegistryKey): Application Compatibility
Cache Windows Registry key.
Returns:
bool: True if the Application Compatibility Cache key was found,
False if not.
"""
value = app_compat_cache_key.GetValueByName("AppCompatCache")
if not value:
logging.warning(
f"Missing AppCompatCache value in key: {app_compat_cache_key.path:s}"
)
return True
value_data = value.data
value_data_size = len(value.data)
# TODO: add non debug output
if self._debug:
self._output_writer.WriteDebugData("Value data:\n", value_data)
format_type = self._parser.CheckSignature(value_data)
if not format_type:
logging.warning("Unsupported signature.")
return True
cache_header = self._parser.ParseHeader(format_type, value_data)
# On Windows Vista and 2008 when the cache is empty it will
# only consist of the header.
if value_data_size <= cache_header.header_size:
return True
cached_entry_offset = cache_header.header_size
cached_entry_index = 0
while cached_entry_offset < value_data_size:
cached_entry = self._parser.ParseCachedEntry(
format_type, value_data, cached_entry_index, cached_entry_offset
)
self.cached_entries.append(cached_entry)
cached_entry_offset += cached_entry.cached_entry_size
cached_entry_index += 1
if (
cache_header.number_of_cached_entries != 0
and cached_entry_index >= cache_header.number_of_cached_entries
):
break
return True
[docs]
def Collect(self, registry, all_control_sets=False):
"""Collects the Application Compatibility Cache.
Args:
registry (dfwinreg.WinRegistry): Windows Registry.
all_control_sets (Optional[bool]): True if the services should be
collected from all control sets instead of only the current control
set.
Returns:
bool: True if the Application Compatibility Cache key was found,
False if not.
"""
result = False
if all_control_sets:
system_key = registry.GetKeyByPath("HKEY_LOCAL_MACHINE\\System\\")
if not system_key:
return result
for control_set_key in system_key.GetSubkeys():
if control_set_key.name.startswith("ControlSet"):
# Windows XP
app_compat_cache_key = control_set_key.GetSubkeyByPath(
"Control\\Session Manager\\AppCompatibility"
)
if app_compat_cache_key:
if self._CollectAppCompatCacheFromKey(app_compat_cache_key):
result = True
# Windows 2003 and later
app_compat_cache_key = control_set_key.GetSubkeyByPath(
"Control\\Session Manager\\AppCompatCache"
)
if app_compat_cache_key:
if self._CollectAppCompatCacheFromKey(app_compat_cache_key):
result = True
else:
# Windows XP
key_path = (
"HKEY_LOCAL_MACHINE\\System\\CurrentControlSet\\Control\\"
"Session Manager\\AppCompatibility"
)
try:
app_compat_cache_key = registry.GetKeyByPath(key_path)
except RuntimeError:
app_compat_cache_key = None
if app_compat_cache_key:
if self._CollectAppCompatCacheFromKey(app_compat_cache_key):
result = True
# Windows 2003 and later
key_path = (
"HKEY_LOCAL_MACHINE\\System\\CurrentControlSet\\Control\\"
"Session Manager\\AppCompatCache"
)
try:
app_compat_cache_key = registry.GetKeyByPath(key_path)
except RuntimeError:
app_compat_cache_key = None
if app_compat_cache_key:
if self._CollectAppCompatCacheFromKey(app_compat_cache_key):
result = True
return result