Source code for volatility3.framework.symbols.windows.extensions.mft

# This file is Copyright 2022 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
from typing import Dict, Iterator, List, Optional, Tuple

from volatility3.framework import constants, exceptions, interfaces, objects

vollog = logging.getLogger(__name__)


[docs] class MFTEntry(objects.StructType): """This represents the base MFT Record""" def __init__( self, context: interfaces.context.ContextInterface, type_name: str, object_info: interfaces.objects.ObjectInformation, size: int, members: Dict[str, Tuple[int, interfaces.objects.Template]], ) -> None: super().__init__(context, type_name, object_info, size, members) self._attrs_loaded = False self._attrs: List[MFTAttribute] = [] @property def symbol_table_name(self) -> str: return self.vol.type_name.split(constants.BANG)[0]
[docs] def get_signature(self) -> objects.String: signature = self.Signature.cast("string", max_length=4, encoding="latin-1") return signature
@property def attributes(self) -> Iterator["MFTAttribute"]: """ Lazily evaluate and yield attributes, caching them in an internal list for re-retrieval. """ if not self._attrs_loaded: self._attrs = list(self._attributes()) self._attrs_loaded = True yield from self._attrs
[docs] def longest_filename(self) -> Optional[objects.String]: names = [name.get_full_name() for name in self.filename_entries()] if not names: return None return max(names, key=lambda x: len(str(x)))
def _attributes(self) -> Iterator["MFTAttribute"]: # We will update this on each pass in the next loop and use it as the new offset. attr_base_offset = self.FirstAttrOffset attribute_object_type_name = ( self.symbol_table_name + constants.BANG + "ATTRIBUTE" ) attr: MFTAttribute = self._context.object( attribute_object_type_name, offset=self.vol.offset + attr_base_offset, layer_name=self.vol.layer_name, ) # There is no field that has a count of Attributes # Keep Attempting to read attributes until we get an invalid attr_header.AttrType try: while attr.Attr_Header.AttrType.is_valid_choice: yield attr # If there's no advancement the loop will never end, so break it now if attr.Attr_Header.Length == 0: break # Update the base offset to point to the next attribute attr_base_offset += attr.Attr_Header.Length # Get the next attribute attr: MFTAttribute = self._context.object( attribute_object_type_name, offset=self.vol.offset + attr_base_offset, layer_name=self.vol.layer_name, ) except exceptions.InvalidAddressException as e: vollog.debug( f"Failed to read attribute at {attr.vol.offset:#x}: {e.__class__.__name__}" ) return
[docs] def standard_information_entries( self, ) -> Iterator[objects.StructType]: """ Yields a STANDARD_INFORMATION struct for each of the STANDARD_INFORMATION attributes in this MFT record (although there should only be one per record). """ for attr in self.attributes: attr_type = attr.Attr_Header.AttrType.lookup() if attr_type != "STANDARD_INFORMATION": continue si_object = ( self.symbol_table_name + constants.BANG + "STANDARD_INFORMATION_ENTRY" ) yield attr.Attr_Data.cast(si_object)
[docs] def filename_entries(self) -> Iterator["MFTFileName"]: """ Yields an MFT Filename for each of the FILE_NAME attributes contained in this MFT record. There are often two - one for the long filename, and the other with the DOS 8.3 short name. """ for attr in self.attributes: try: attr_type = attr.Attr_Header.AttrType.lookup() if attr_type != "FILE_NAME": continue fn_object = self.symbol_table_name + constants.BANG + "FILE_NAME_ENTRY" attr_data = attr.Attr_Data.cast(fn_object) except exceptions.InvalidAddressException as e: vollog.debug( f"Failed to read attr at {attr.vol.offset:#x}: {e.__class__.__name__}" ) continue yield attr_data
def _data_attributes(self): for attr in self.attributes: if not ( attr.Attr_Header.AttrType.lookup() == "DATA" and attr.Attr_Header.NonResidentFlag == 0 ): continue yield attr
[docs] def resident_data_attributes(self) -> Iterator["MFTAttribute"]: """ Yields all MFT attributes that contain resident data for the primary stream. """ for attr in self._data_attributes(): if attr.Attr_Header.NameLength == 0: yield attr
[docs] def alternate_data_streams(self) -> Iterator["MFTAttribute"]: """ Yields all MFT attributes that contain alternate data streams (ADS). """ for attr in self._data_attributes(): if attr.Attr_Header.NameLength != 0: yield attr
[docs] class MFTFileName(objects.StructType): """This represents an MFT $FILE_NAME Attribute"""
[docs] def get_full_name(self) -> objects.String: """ Returns the UTF-16 decoded filename. """ output = self.Name.cast( "string", encoding="utf16", max_length=self.NameLength * 2, errors="replace" ) return output
[docs] class MFTAttribute(objects.StructType): """This represents an MFT ATTRIBUTE"""
[docs] def get_resident_filename(self) -> Optional[objects.String]: """ Returns the resident filename (typically for an Alternate Data Stream (ADS)). """ # 4MB chosen as cutoff instead of 4KB to allow for recovery from format /L created file systems # Length as 512 as its 256*2, which is the maximum size for an entire file path, so this is even generous if ( self.Attr_Header.ContentOffset > 0x400000 or self.Attr_Header.NameLength > 512 ): return None # To get the resident name, we jump to relative name offset and read name length * 2 bytes of data try: name = self._context.object( self.vol.type_name.split(constants.BANG)[0] + constants.BANG + "string", layer_name=self.vol.layer_name, offset=self.vol.offset + self.Attr_Header.NameOffset, max_length=self.Attr_Header.NameLength * 2, errors="replace", encoding="utf16", ) return name except exceptions.InvalidAddressException as e: vollog.debug( f"Failed to get resident file content due to {e.__class__.__name__}" ) return None
[docs] def get_resident_filecontent(self) -> Optional[objects.Bytes]: """ Returns the file content that is resident within this MFT attribute, for either the primary or an alternate data stream. """ # smear observed in mass testing of samples # 4MB chosen as cutoff instead of 4KB to allow for recovery from format /L created file systems if ( self.Attr_Header.ContentOffset > 0x400000 or self.Attr_Header.ContentLength > 0x400000 ): return None # To get the resident content, we jump to relative content offset and read name length * 2 bytes of data try: bytesobj = self._context.object( self.vol.type_name.split(constants.BANG)[0] + constants.BANG + "bytes", layer_name=self.vol.layer_name, offset=self.vol.offset + self.Attr_Header.ContentOffset, native_layer_name=self.vol.native_layer_name, length=self.Attr_Header.ContentLength, ) return bytesobj except exceptions.InvalidAddressException as e: vollog.debug( f"Failed to get resident file content due to {e.__class__.__name__}" ) return None