# 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
#
import dataclasses
import datetime
import enum
import itertools
import logging
from typing import Dict, Iterable, Iterator, List, Optional, Tuple, Union
from volatility3.framework import interfaces, renderers
from volatility3.framework.configuration import requirements
from volatility3.framework.layers import registry
from volatility3.framework.renderers import conversion
from volatility3.framework.symbols.windows.extensions import registry as reg_extensions
from volatility3.plugins import timeliner
from volatility3.plugins.windows.registry import hivelist
vollog = logging.getLogger(__name__)
#######################################################################
# More information about the following enums can be found in the report
# 'Analysis of the AmCache` by Blanche Lagny, 2019
#######################################################################
[docs]
class Win8FileValName(enum.Enum):
"""
An enumeration that creates a helpful mapping of opaque Windows 8 Amcache
'File' subkey value names to their human-readable equivalent.
"""
ProgramID = "100"
SHA1Hash = "101"
Product = "0"
Company = "1"
Size = "6"
SizeOfImage = "7"
PEHeaderChecksum = "9"
LastModTime = "11" # REG_QWORD FILETIME
CreateTime = "12" # REG_QWORD FILETIME
Path = "15"
LastModTime2 = "17" # REG_QWORD FILETIME
Version = "d"
CompileTime = "f" # REG_QWORD UNIX EPOCH
[docs]
class Win8ProgramValName(enum.Enum):
"""
An enumeration that creates a helpful mapping of opaque Windows 8 Amcache
'Program' subkey value names to their human-readable equivalent.
"""
Product = "0"
Version = "1"
Publisher = "2"
InstallTime = "a"
MSIProductCode = "11"
MSIPackageCode = "12"
ProductCode = "f"
PackageCode = "10"
[docs]
class Win10InvAppFileValName(enum.Enum):
"""
An enumeration containing the most useful Windows 10 Amcache
'InventoryApplicationFile' subkey value names.
"""
FileId = "FileId"
LinkDate = "LinkDate"
LowerCaseLongPath = "LowerCaseLongPath"
ProductName = "ProductName"
ProductVersion = "ProductVersion"
ProgramID = "ProgramId"
Publisher = "Publisher"
[docs]
class Win10InvAppValName(enum.Enum):
"""
An enumeration containing the most useful Windows 10 Amcache
'InventoryApplication' subkey value names.
"""
InstallDate = "InstallDate"
Name = "Name"
Publisher = "Publisher"
RootDirPath = "RootDirPath"
Version = "Version"
[docs]
class Win10DriverBinaryValName(enum.Enum):
"""
An enumeration containing the most useful Windows 10 Amcache
'InventoryDriverBinary' subkey value names.
"""
DriverId = "DriverId"
DriverName = "DriverName"
DriverCompany = "DriverCompany"
Product = "Product"
Service = "Service"
DriverTimeStamp = "DriverTimeStamp"
[docs]
class AmcacheEntryType(enum.IntEnum):
Driver = 1
Program = 2
File = 3
NullableString = Union[str, None, interfaces.renderers.BaseAbsentValue]
NullableDatetime = Union[datetime.datetime, None, interfaces.renderers.BaseAbsentValue]
@dataclasses.dataclass
class _AmcacheEntry:
"""
A class containing all information about an entry from the Amcache registry hive.
Because all values could potentially be paged out of memory or malformed, they are all
a union between their expected value and `interfaces.renderers.BaseAbsentValue`.
"""
entry_type: str
path: NullableString = renderers.NotApplicableValue()
company: NullableString = renderers.NotApplicableValue()
last_modify_time: NullableDatetime = renderers.NotApplicableValue()
last_modify_time_2: NullableDatetime = renderers.NotApplicableValue()
install_time: NullableDatetime = renderers.NotApplicableValue()
compile_time: NullableDatetime = renderers.NotApplicableValue()
sha1_hash: NullableString = renderers.NotApplicableValue()
service: NullableString = renderers.NotApplicableValue()
product_name: NullableString = renderers.NotApplicableValue()
product_version: NullableString = renderers.NotApplicableValue()
def _entry_sort_key(entry_tuple: Tuple[NullableString, _AmcacheEntry]) -> str:
"""Sorts entries by program ID. This is broken out as a function here
to ensure consistency in sorting between the `group_by` and `sorted` function
invocations.
"""
program_id, _ = entry_tuple
key = program_id if isinstance(program_id, str) else ""
return key
def _get_string_value(
values: Dict[str, reg_extensions.CM_KEY_VALUE], name: str
) -> NullableString:
try:
value = values[name]
except KeyError:
return renderers.NotAvailableValue()
data = value.decode_data()
if not isinstance(data, bytes):
return renderers.UnparsableValue()
return data.decode("utf-16le", errors="replace").rstrip("\u0000")
def _get_datetime_filetime_value(
values: Dict[str, reg_extensions.CM_KEY_VALUE], name: str
) -> NullableDatetime:
try:
value = values[name]
except KeyError:
return renderers.NotAvailableValue()
data = value.decode_data()
if not isinstance(data, int):
return renderers.UnparsableValue()
return conversion.wintime_to_datetime(data)
def _get_datetime_utc_epoch_value(
values: Dict[str, reg_extensions.CM_KEY_VALUE], name: str
) -> NullableDatetime:
try:
value = values[name]
except KeyError:
return renderers.NotAvailableValue()
data = value.decode_data()
if not isinstance(data, (int, float)):
return renderers.UnparsableValue()
try:
return datetime.datetime.fromtimestamp(float(data), datetime.timezone.utc)
except (ValueError, OverflowError, OSError):
return renderers.UnparsableValue()
def _get_datetime_str_value(
values: Dict[str, reg_extensions.CM_KEY_VALUE], name: str
) -> NullableDatetime:
try:
value = values[name]
except KeyError:
return renderers.NotAvailableValue()
data = value.decode_data()
if not isinstance(data, int):
return renderers.UnparsableValue()
if isinstance(data, str):
try:
return datetime.datetime.strptime(data, "%m/%d/%Y %H:%M:%S")
except ValueError:
return renderers.UnparsableValue()
else:
return renderers.UnparsableValue()
[docs]
class Amcache(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface):
"""Extract information on executed applications from the AmCache."""
_required_framework_version = (2, 0, 0)
_version = (1, 0, 0)
[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.PluginRequirement(
name="hivelist", plugin=hivelist.HiveList, version=(1, 0, 0)
),
]
[docs]
def generate_timeline(
self,
) -> Iterator[Tuple[str, timeliner.TimeLinerType, datetime.datetime]]:
for _, entry in self._generator():
if isinstance(entry.last_modify_time, datetime.datetime):
yield f"Amcache: {entry.entry_type} {entry.path} registry key modified", timeliner.TimeLinerType.MODIFIED, entry.last_modify_time
if isinstance(entry.last_modify_time_2, datetime.datetime):
yield f"Amcache: {entry.entry_type} {entry.path} STANDARD_INFORMATION create time", timeliner.TimeLinerType.CREATED, entry.last_modify_time_2
if isinstance(entry.install_time, datetime.datetime):
yield f"Amcache: {entry.entry_type} {entry.path} installed", timeliner.TimeLinerType.CREATED, entry.install_time
if isinstance(entry.compile_time, datetime.datetime):
yield f"Amcache: {entry.entry_type} {entry.path} compiled (PE metadata)", timeliner.TimeLinerType.MODIFIED, entry.compile_time
[docs]
@classmethod
def get_amcache_hive(
cls,
context: interfaces.context.ContextInterface,
config_path: str,
kernel: interfaces.context.ModuleInterface,
) -> Optional[registry.RegistryHive]:
"""Retrieves the `Amcache.hve` registry hive from the kernel module, if it can be located."""
return next(
hivelist.HiveList.list_hives(
context=context,
base_config_path=interfaces.configuration.path_join(
config_path, "hivelist"
),
layer_name=kernel.layer_name,
symbol_table=kernel.symbol_table_name,
filter_string="amcache",
),
None,
)
[docs]
@classmethod
def parse_file_key(
cls, file_key: reg_extensions.CM_KEY_NODE
) -> Iterator[Tuple[NullableString, _AmcacheEntry]]:
"""Parses File entries from the Windows 8 `Root\\File` key.
:param programs_key: The `Root\\File` registry key.
:return: An iterator of tuples, where the first member is the program ID string for
correlating `Root\\Program` entries, and the second member is the `AmcacheEntry`.
"""
val_enum = Win8FileValName
wanted_values = [key.value for key in val_enum]
for file_entry_key in itertools.chain(
*(key.get_subkeys() for key in file_key.get_subkeys())
):
vollog.debug(f"Checking Win8 File key {file_entry_key.get_name()}")
values = {
str(value.get_name()): value
for value in file_entry_key.get_values()
if value.get_name() in wanted_values
}
program_id = _get_string_value(values, val_enum.ProgramID.value)
path = _get_string_value(values, val_enum.Path.value)
company = _get_string_value(values, val_enum.Company.value)
last_mod_time = _get_datetime_filetime_value(
values, val_enum.LastModTime.value
)
last_mod_time_2 = _get_datetime_filetime_value(
values, val_enum.LastModTime2.value
)
install_time = _get_datetime_filetime_value(
values, val_enum.CreateTime.value
)
compile_time = _get_datetime_utc_epoch_value(
values, val_enum.CompileTime.value
)
sha1_hash = _get_string_value(values, val_enum.SHA1Hash.value)
vollog.debug(f"Found sha1hash {sha1_hash}")
product_name = _get_string_value(values, val_enum.Product.value)
yield program_id, _AmcacheEntry(
AmcacheEntryType.File.name,
path=path,
company=company,
last_modify_time=last_mod_time,
last_modify_time_2=last_mod_time_2,
install_time=install_time,
compile_time=compile_time,
sha1_hash=(
sha1_hash.lstrip("0000")
if isinstance(sha1_hash, str)
else sha1_hash
),
product_name=product_name,
)
[docs]
@classmethod
def parse_programs_key(
cls, programs_key: reg_extensions.CM_KEY_NODE
) -> Iterator[Tuple[str, _AmcacheEntry]]:
"""Parses Program entries from the Windows 8 `Root\\Programs` key.
:param programs_key: The `Root\\Programs` registry key.
:return: An iterator of tuples, where the first member is the program ID string for
correlating `Root\\File` entries, and the second member is the `AmcacheEntry`.
"""
val_enum = Win8ProgramValName
wanted_values = [key.value for key in val_enum]
for program_key in programs_key.get_subkeys():
values = {
str(value.get_name()): value
for value in program_key.get_values()
if value.get_name() in wanted_values
}
vollog.debug(f"Parsing Win8 Program key {program_key.get_name()}")
program_id = program_key.get_name().strip().strip("\u0000")
product = _get_string_value(values, val_enum.Product.value)
company = _get_string_value(values, val_enum.Publisher.value)
install_time = _get_datetime_utc_epoch_value(
values, val_enum.InstallTime.value
)
version = _get_string_value(values, val_enum.Version.value)
yield program_id, _AmcacheEntry(
AmcacheEntryType.Program.name,
company=company,
last_modify_time=conversion.wintime_to_datetime(
program_key.LastWriteTime.QuadPart
),
install_time=install_time,
product_name=product,
product_version=version,
)
[docs]
@classmethod
def parse_inventory_app_key(
cls, inv_app_key: reg_extensions.CM_KEY_NODE
) -> Iterator[Tuple[str, _AmcacheEntry]]:
"""Parses InventoryApplication entries from the Windows 10 `Root\\InventoryApplication` key.
:param programs_key: The `Root\\InventoryApplication` registry key.
:return: An iterator of tuples, where the first member is the program ID string for
correlating `Root\\InventoryApplicationFile` entries, and the second member is the `AmcacheEntry`.
"""
val_enum = Win10InvAppValName
wanted_values = [key.value for key in val_enum]
for program_key in inv_app_key.get_subkeys():
program_id = program_key.get_name()
values = {
str(value.get_name()): value
for value in program_key.get_values()
if value.get_name() in wanted_values
}
name = _get_string_value(values, val_enum.Name.value)
version = _get_string_value(values, val_enum.Version.value)
publisher = _get_string_value(values, val_enum.Publisher.value)
path = _get_string_value(values, val_enum.RootDirPath.value)
install_date = _get_datetime_str_value(values, val_enum.InstallDate.value)
last_mod = conversion.wintime_to_datetime(
program_key.LastWriteTime.QuadPart
)
product: str = name if isinstance(name, str) else "UNKNOWN" # type: ignore
yield program_id.strip().strip("\u0000"), _AmcacheEntry(
AmcacheEntryType.Program.name,
path=path,
last_modify_time=last_mod,
install_time=install_date,
product_name=product,
company=publisher,
product_version=version,
)
[docs]
@classmethod
def parse_inventory_app_file_key(
cls, inv_app_file_key: reg_extensions.CM_KEY_NODE
) -> Iterator[Tuple[NullableString, _AmcacheEntry]]:
"""Parses executable file entries from the `Root\\InventoryApplicationFile` registry key.
:param inv_app_file_key: The `Root\\InventoryApplicationFile` registry key.
:return: An iterator of tuples, where the first member is the program ID string for correlating
with it's parent `InventoryApplication` program entry, and the second member is the `Amcache` entry.
"""
val_enum = Win10InvAppFileValName
wanted_values = [key.value for key in val_enum]
for file_key in inv_app_file_key.get_subkeys():
vollog.debug(
f"Parsing Win10 InventoryApplicationFile key {file_key.get_name()}"
)
values = {
str(value.get_name()): value
for value in file_key.get_values()
if value.get_name() in wanted_values
}
last_mod = conversion.wintime_to_datetime(file_key.LastWriteTime.QuadPart)
path = _get_string_value(values, val_enum.LowerCaseLongPath.value)
linkdate = _get_datetime_str_value(values, val_enum.LinkDate.value)
sha1_hash = _get_string_value(values, val_enum.FileId.value)
publisher = _get_string_value(values, val_enum.Publisher.value)
prod_name = _get_string_value(values, val_enum.ProductName.value)
prod_ver = _get_string_value(values, val_enum.ProductVersion.value)
program_id = _get_string_value(values, val_enum.ProgramID.value)
yield program_id, _AmcacheEntry(
AmcacheEntryType.File.name,
path=path,
company=publisher,
last_modify_time=last_mod,
compile_time=linkdate,
sha1_hash=(
sha1_hash.lstrip("0000")
if isinstance(sha1_hash, str)
else sha1_hash
),
product_name=prod_name,
product_version=prod_ver,
)
[docs]
@classmethod
def parse_driver_binary_key(
cls, driver_binary_key: reg_extensions.CM_KEY_NODE
) -> Iterator[_AmcacheEntry]:
"""Parses information about installed drivers from the `Root\\InventoryDriverBinary` registry key.
:param driver_binary_key: The `Root\\InventoryDriverBinary` registry key
:return: An iterator of `AmcacheEntry`s
"""
val_enum = Win10DriverBinaryValName
wanted_values = [key.value for key in val_enum]
for binary_key in driver_binary_key.get_subkeys():
values = {
str(value.get_name()): value
for value in binary_key.get_values()
if value.get_name() in wanted_values
}
# Depending on the Windows version, the key name will be either the name
# of the driver, or its SHA1 hash.
if "/" in str(binary_key.get_name()):
driver_name = str(binary_key.get_name())
sha1_hash = _get_string_value(values, val_enum.DriverId.name)
else:
sha1_hash = str(binary_key.get_name())
driver_name = _get_string_value(values, val_enum.DriverName.name)
if isinstance(sha1_hash, str):
sha1_hash = sha1_hash[4:] if sha1_hash.startswith("0000") else sha1_hash
company, product, service, last_write_time, driver_timestamp = (
_get_string_value(values, val_enum.DriverCompany.name),
_get_string_value(values, val_enum.Product.name),
_get_string_value(values, val_enum.Service.name),
conversion.wintime_to_datetime(binary_key.LastWriteTime.QuadPart),
_get_datetime_utc_epoch_value(values, val_enum.DriverTimeStamp.name),
)
yield _AmcacheEntry(
entry_type=AmcacheEntryType.Driver.name,
path=driver_name,
company=company,
last_modify_time=last_write_time,
compile_time=driver_timestamp,
sha1_hash=(
sha1_hash.lstrip("0000")
if isinstance(sha1_hash, str)
else sha1_hash
),
service=service,
product_name=product,
)
def _generator(self) -> Iterator[Tuple[int, _AmcacheEntry]]:
kernel = self.context.modules[self.config["kernel"]]
def indented(
entry_gen: Iterable[_AmcacheEntry], indent: int = 0
) -> Iterator[Tuple[int, _AmcacheEntry]]:
for item in entry_gen:
yield indent, item
# Building the dictionary ahead of time is much better for performance
# vs looking up each service's DLL individually.
amcache = self.get_amcache_hive(self.context, self.config_path, kernel)
if amcache is None:
return
try:
yield from indented(
self.parse_driver_binary_key(
amcache.get_key("Root\\InventoryDriverBinary") # type: ignore
)
)
except (KeyError, registry.RegistryFormatException):
# Registry key not found
pass
try:
programs: Dict[str, _AmcacheEntry] = {
program_id: entry
for program_id, entry in self.parse_programs_key(
amcache.get_key("Root\\Programs")
) # type: ignore
}
except (KeyError, registry.RegistryFormatException):
programs = {}
try:
files = sorted(
list(
self.parse_file_key(amcache.get_key("Root\\File")), # type: ignore
),
key=_entry_sort_key,
)
except (KeyError, registry.RegistryFormatException):
files = []
for program_id, file_entries in itertools.groupby(
files,
key=_entry_sort_key,
):
files_indent = 0
if isinstance(program_id, str):
try:
program_entry = programs.pop(program_id.strip().strip("\u0000"))
yield (0, program_entry)
files_indent = 1
except KeyError:
# No parent program for this file entry
pass
for _, entry in file_entries:
yield files_indent, entry
for empty_program in programs.values():
yield 0, empty_program
try:
programs: Dict[str, _AmcacheEntry] = dict(
self.parse_inventory_app_key(
amcache.get_key("Root\\InventoryApplication") # type: ignore
)
)
except (KeyError, registry.RegistryFormatException):
programs = {}
try:
files = sorted(
list(
self.parse_inventory_app_file_key(amcache.get_key("Root\\InventoryApplicationFile")), # type: ignore
),
key=_entry_sort_key,
)
except (KeyError, registry.RegistryFormatException):
files = []
for program_id, file_entries in itertools.groupby(
files,
key=_entry_sort_key,
):
files_indent = 0
if isinstance(program_id, str):
try:
program_entry = programs.pop(program_id.strip().strip("\u0000"))
yield (0, program_entry)
files_indent = 1
except KeyError:
# No parent program for this file entry
pass
for _, entry in file_entries:
yield files_indent, entry
for empty_program in programs.values():
yield 0, empty_program
[docs]
def run(self):
return renderers.TreeGrid(
[
("EntryType", str),
("Path", str),
("Company", str),
("LastModifyTime", datetime.datetime),
("LastModifyTime2", datetime.datetime),
("InstallTime", datetime.datetime),
("CompileTime", datetime.datetime),
("SHA1", str),
("Service", str),
("ProductName", str),
("ProductVersion", str),
],
(
(indent, dataclasses.astuple(entry))
for indent, entry in self._generator()
),
)