MFT Plugin use ISF instead of Struct

This commit is contained in:
KevTheHermit
2022-01-09 22:04:13 +00:00
parent 295fb453f5
commit 9d86599e7f
4 changed files with 577 additions and 265 deletions
+87 -265
View File
@@ -4,95 +4,20 @@
import datetime
import logging
import struct
from typing import Dict
from volatility3.framework import constants, renderers, interfaces
from volatility3.framework.configuration import requirements
from volatility3.framework import exceptions
from volatility3.framework.objects import utility
from volatility3.framework.renderers import conversion, format_hints
from volatility3.framework.symbols.windows.extensions.mft import AttributeTypes, NameSpace, PermissionFlags, MFTFlags
from volatility3.framework.symbols.windows.mft import MFTIntermedSymbols
from volatility3.plugins import yarascan
vollog = logging.getLogger(__name__)
try:
import yara
except ImportError:
vollog.info("Python Yara module not found, plugin (and dependent plugins) not available")
raise
signatures = {
'mft_objects': """rule mft_headers
{
strings:
$header1 = "FILE0"
$header2 = "FILE*"
$header3 = "BAAD"
condition:
any of them
}"""
}
# https://github.com/volatilityfoundation/volatility/blob/a438e768194a9e05eb4d9ee9338b881c0fa25937/volatility/plugins/mftparser.py#L60
ATTRIBUTE_TYPE_ID = {
0x10:"STANDARD_INFORMATION",
0x20:"ATTRIBUTE_LIST",
0x30:"FILE_NAME",
0x40:"OBJECT_ID",
0x50:"SECURITY_DESCRIPTOR",
0x60:"VOLUME_NAME",
0x70:"VOLUME_INFORMATION",
0x80:"DATA",
0x90:"INDEX_ROOT",
0xa0:"INDEX_ALLOCATION",
0xb0:"BITMAP",
0xc0:"REPARSE_POINT",
0xd0:"EA_INFORMATION", #Extended Attribute
0xe0:"EA",
0xf0:"PROPERTY_SET",
0x100:"LOGGED_UTILITY_STREAM",
}
VERBOSE_STANDARD_INFO_FLAGS = {
0x1:"Read Only",
0x2:"Hidden",
0x4:"System",
0x20:"Archive",
0x40:"Device",
0x80:"Normal",
0x100:"Temporary",
0x200:"Sparse File",
0x400:"Reparse Point",
0x800:"Compressed",
0x1000:"Offline",
0x2000:"Content not indexed",
0x4000:"Encrypted",
0x10000000:"Directory",
0x20000000:"Index view",
}
FILE_NAME_NAMESPACE = {
0x0:"POSIX", # Case sensitive, allows all Unicode chars except '/' and NULL
0x1:"Win32", # Case insensitive, allows most Unicide except specials ('/', '\', ';', '>', '<', '?')
0x2:"DOS", # Case insensitive, upper case, no special chars, name is 8 or fewer chars in name and 3 or less extension
0x3:"Win32 & DOS", # Used when original name fits in DOS namespace and 2 names are not needed
}
MFT_FLAGS = {
0x0: "Removed",
0x1: "File", # "In Use",
0x2: "Directory", # if flag & 0x0002 == 0 this is a regular file
0x3: "Directory"
}
INDEX_ENTRY_FLAGS = {
0x1:"Child Node Exists",
0x2:"Last entry in list",
}
class MFTScan(interfaces.plugins.PluginInterface):
"""Scans for MFT FILE objects present in a particular windows memory image."""
@@ -108,198 +33,94 @@ class MFTScan(interfaces.plugins.PluginInterface):
version = (2, 0, 0)),
]
# https://docs.python.org/3/library/struct.html
@classmethod
def unpack_data(self, mft_record: bytes, offset: int, data_type: str) -> bytes:
"""Helper to unpack values from the raw mft_record
Args:
mft_record: 1024 bytes starting from header value as returned by layer read
offset: how far in to the record to read
data_type: what is the data type to unpack
Returns:
bytes: the unpacked data
"""
if data_type == 'unsigned long':
return struct.unpack('<L', mft_record[offset:offset+4])[0]
elif data_type == 'unsigned short':
return struct.unpack('<H', mft_record[offset:offset+2])[0]
elif data_type == 'unsigned long long':
return struct.unpack('<Q', mft_record[offset:offset+8])[0]
elif data_type == 'unsigned char':
return struct.unpack('<B', mft_record[offset:offset+1])[0]
elif data_type == 'int':
return struct.unpack('<I', mft_record[offset:offset+4])[0]
@classmethod
def parse_mft_record(self, mft_record: bytes) -> Dict:
"""Takes an MFT Record and attempts to parse, MFT, SI and FN attributes
Args:
mft_record: 1024 bytes starting from header value as returned by layer read
Returns:
Dict: a Dictionary that contains the Parse MFT Record
"""
# https://github.com/Invoke-IR/ForensicPosters
flags = self.unpack_data(mft_record, 22, 'unsigned short')
file_type = MFT_FLAGS.get(flags, 'Unknown')
mft_entry = {
"signature": mft_record[:4].decode(),
"FixupArrayOffset": self.unpack_data(mft_record, 4, 'unsigned short'),
"NumFixupEntries": self.unpack_data(mft_record, 6, 'unsigned short'),
"LSN": self.unpack_data(mft_record, 8, 'unsigned long long'),
"SequenceValue": self.unpack_data(mft_record, 16, 'unsigned short'),
"link_count": self.unpack_data(mft_record, 18, 'unsigned short'),
"FirstAttrOffset": self.unpack_data(mft_record, 20, 'unsigned short'),
"flags": file_type,
"record_number": self.unpack_data(mft_record, 44, 'unsigned long'),
"attributes": {
"SI": {},
"FN": []
}
}
attr_offset = mft_entry['FirstAttrOffset']
# Check at most for 6 entries
for i in range(6):
# If we attempt to overread the entry continue out
if attr_offset > 1000:
continue
# attr_header
attr_type = self.unpack_data(mft_record, attr_offset, 'int')
attr_len = self.unpack_data(mft_record, attr_offset+4, 'int')
# As we look for strucutres of header + 1K we can not unpack non resident structures
nr_flag = self.unpack_data(mft_record, attr_offset+8, 'unsigned char')
# Skip headers
attr_data = attr_offset+24 # Len of Common and Resident Headers
if attr_type in ATTRIBUTE_TYPE_ID:
vollog.debug(f'Found Attribute {ATTRIBUTE_TYPE_ID[attr_type]}')
if ATTRIBUTE_TYPE_ID[attr_type] == 'STANDARD_INFORMATION':
creation_time_win = self.unpack_data(mft_record, attr_data, 'unsigned long long')
modified_time_win = self.unpack_data(mft_record, attr_data+8, 'unsigned long long')
altered_time_win = self.unpack_data(mft_record, attr_data+16, 'unsigned long long')
access_time_win = self.unpack_data(mft_record, attr_data+24, 'unsigned long long')
flags = self.unpack_data(mft_record, attr_data+32, 'unsigned short')
permissions = VERBOSE_STANDARD_INFO_FLAGS.get(flags, 'Unknown')
mft_entry['attributes']['SI'] = {
"creation_time": conversion.wintime_to_datetime(creation_time_win),
"modified_time": conversion.wintime_to_datetime(modified_time_win),
"updated_time": conversion.wintime_to_datetime(altered_time_win),
"accessed_time": conversion.wintime_to_datetime(access_time_win),
"flags": permissions
}
if ATTRIBUTE_TYPE_ID[attr_type] == 'FILE_NAME':
parent_record = self.unpack_data(mft_record, attr_data, 'unsigned long long')
creation_time_win = self.unpack_data(mft_record, attr_data+8, 'unsigned long long')
modified_time_win = self.unpack_data(mft_record, attr_data+16, 'unsigned long long')
altered_time_win = self.unpack_data(mft_record, attr_data+24, 'unsigned long long')
access_time_win = self.unpack_data(mft_record, attr_data+32, 'unsigned long long')
name_len = self.unpack_data(mft_record, attr_data+64, 'unsigned char')
name_space = self.unpack_data(mft_record, attr_data+65, 'unsigned char')
# Unicode and partially corruprted records can break us here.
file_name = mft_record[attr_data+66:attr_data+66+(2*name_len)]
#file_name = utility.array_to_string(file_name)
try:
file_name = file_name.replace(b'\x00', b'').decode()
except:
file_name = str(file_name.replace(b'\x00', b''))
flags = self.unpack_data(mft_record, attr_data+56, 'unsigned short')
permissions = VERBOSE_STANDARD_INFO_FLAGS.get(flags, 'Unknown')
mft_entry['attributes']['FN'].append(
{
"creation_time": conversion.wintime_to_datetime(creation_time_win),
"modified_time": conversion.wintime_to_datetime(modified_time_win),
"updated_time": conversion.wintime_to_datetime(altered_time_win),
"accessed_time": conversion.wintime_to_datetime(access_time_win),
"allocated_size": self.unpack_data(mft_record, attr_data+40, 'unsigned long long'),
"real_size": self.unpack_data(mft_record, attr_data+48, 'unsigned long long'),
"flags": permissions,
"file_name": file_name,
"name_space": name_space
})
# Update Offset for next Attribute
attr_offset += attr_len
return mft_entry
def _generator(self):
rules = yara.compile(sources = signatures)
layer = self.context.layers[self.config['primary']]
# Yara Rule to scan for MFT Header Signatures
rules = yarascan.YaraScan.process_yara_options({'yara_rules': '/FILE0|FILE\*|BAAD/'})
# Read in the Symbol File
symbol_table = MFTIntermedSymbols.create(
self.context,
self.config_path,
"windows",
"mft"
)
# get each of the individual Field Sets
mft_object = symbol_table + constants.BANG + "MFT_ENTRY"
header_object = symbol_table + constants.BANG + "ATTR_HEADER"
si_object = symbol_table + constants.BANG + "STANDARD_INFORMATION_ENTRY"
fn_object = symbol_table + constants.BANG + "FILE_NAME_ENTRY"
# Scan the layer for Raw MFT records and parse the fields
for offset, rule_name, name, value in layer.scan(context = self.context, scanner = yarascan.YaraScanner(rules = rules)):
# For each matching rule try to read 1024 bytes (size of an MFT record) at the offset.
try:
mft_record = layer.read(offset, 1024, False)
mft_entry = self.parse_mft_record(mft_record)
mft_record = self.context.object(mft_object, offset=offset, layer_name=layer.name)
# We will update this on each pass in the next loop and use it as the new offset.
attr_base_offset = mft_record.FirstAttrOffset
# There is no field that has a count of Attributes
# Keep Attempting to read attributes until we get an invalid attr_header.AttrType
while True:
attr_header = self.context.object(header_object, offset=offset+attr_base_offset, layer_name=layer.name)
attr_resident_header = self.context.object(header_object, offset=offset+attr_base_offset+16, layer_name=layer.name)
vollog.debug(f"Attr Type: {attr_header.AttrType}")
# If this is not a valid type then exit the loop
if not AttributeTypes(attr_header.AttrType).value:
break
# Offset past the headers to the attribute data
attr_data_offset = offset+attr_base_offset+24
# Standard Information Attribute
if attr_header.AttrType == 0x10:
attr_data = self.context.object(si_object, offset=attr_data_offset, layer_name=layer.name)
yield 0, (
format_hints.Hex(attr_data_offset),
mft_record.get_signature(),
mft_record.RecordNumber,
mft_record.LinkCount,
MFTFlags(mft_record.Flags).name,
renderers.NotApplicableValue(),
AttributeTypes(attr_header.AttrType).name,
conversion.wintime_to_datetime(attr_data.CreationTime),
conversion.wintime_to_datetime(attr_data.ModifiedTime),
conversion.wintime_to_datetime(attr_data.UpdatedTime),
conversion.wintime_to_datetime(attr_data.AccessedTime),
renderers.NotApplicableValue(),
)
# File Name Attribute
if attr_header.AttrType == 0x30:
attr_data = self.context.object(fn_object, offset=attr_data_offset, layer_name=layer.name)
file_name = attr_data.get_full_name()
yield 1, (
format_hints.Hex(attr_data_offset),
mft_record.get_signature(),
mft_record.RecordNumber,
mft_record.LinkCount,
MFTFlags(mft_record.Flags).name,
PermissionFlags(attr_data.Flags).name,
AttributeTypes(attr_header.AttrType).name,
conversion.wintime_to_datetime(attr_data.CreationTime),
conversion.wintime_to_datetime(attr_data.ModifiedTime),
conversion.wintime_to_datetime(attr_data.UpdatedTime),
conversion.wintime_to_datetime(attr_data.AccessedTime),
file_name
)
# Update the base offset to point to the next attribute
attr_base_offset += attr_header.Length
except exceptions.PagedInvalidAddressException:
mft_entry = None
#except Exception as err:
# vollog.error(err)
# mft_entry = None
pass
if mft_entry:
vollog.debug(mft_entry)
# Tree Grid is large and variable
si = mft_entry['attributes']['SI']
fn = mft_entry['attributes']['FN']
signature = mft_entry.get('signature', renderers.NotAvailableValue())
record_number = mft_entry.get('record_number', renderers.NotAvailableValue())
link_count = mft_entry.get('link_count', renderers.NotAvailableValue())
permissions = mft_entry.get('flags', renderers.NotAvailableValue())
si_creation_time = si.get('creation_time', renderers.NotAvailableValue())
si_modified_time = si.get('modified_time', renderers.NotAvailableValue())
si_updated_time = si.get('updated_time', renderers.NotAvailableValue())
si_accessed_time = si.get('accessed_time', renderers.NotAvailableValue())
yield 0, (
format_hints.Hex(offset),
signature,
record_number,
link_count,
permissions,
'Standard Information',
renderers.NotApplicableValue(),
si_creation_time,
si_modified_time,
si_updated_time,
si_accessed_time)
for entry in fn:
# As this is variable and may or may not exist
# And could have 0-6 entries lets do it per row.
yield 1, (
format_hints.Hex(offset),
signature,
record_number,
link_count,
permissions,
'FileName',
entry.get('file_name',renderers.NotAvailableValue()),
entry.get('creation_time', renderers.NotAvailableValue()),
entry.get('modified_time', renderers.NotAvailableValue()),
entry.get('updated_time', renderers.NotAvailableValue()),
entry.get('accessed_time', renderers.NotAvailableValue()))
def run(self):
return renderers.TreeGrid([
@@ -307,11 +128,12 @@ class MFTScan(interfaces.plugins.PluginInterface):
('Record Type', str),
('Record Number', int),
('Link Count', int),
('MFT Type', str),
('Permissions', str),
('Attribute Type', str),
('Filename', str),
('Created', datetime.datetime),
('Modified', datetime.datetime),
('Updated', datetime.datetime),
('Accessed', datetime.datetime)
('Accessed', datetime.datetime),
('Filename', str),
],self._generator())
@@ -0,0 +1,104 @@
# 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 enum
from volatility3.framework import exceptions, objects, renderers
from volatility3.framework.objects import utility
class AttributeTypes(enum.Enum):
STANDARD_INFORMATION = 0x10
ATTRIBUTE_LIST = 0x20
FILE_NAME = 0x30
OBJECT_ID = 0x40
SECURITY_DESCRIPTOR = 0x50
VOLUME_NAME = 0x60
VOLUME_INFORMATION = 0x70
DATA = 0x80
INDEX_ROOT = 0x90
INDEX_ALLOCATION = 0xa0
BITMAP = 0xb0
REPARSE_POINT = 0xc0
EA_INFORMATION = 0xd0
EA = 0xe0
PROPERTY_SET = 0xf0
LOGGED_UTILITY_STREAM = 0x100
Unknown = None
@classmethod
def _missing_(cls, value):
return cls(AttributeTypes.Unknown)
class NameSpace(enum.Enum):
POSIX = 0x0
Win32 = 0x1
DOS = 0x2
Win32DOS = 0x3
Unknown = None
@classmethod
def _missing_(cls, value):
return cls(NameSpace.Unknown)
class MFTFlags(enum.Enum):
Removed = 0x00
File = 0x1
Directory = 0x2
DirInUse = 0x3
Unknown = None
@classmethod
def _missing_(cls, value):
return cls(MFTFlags.Unknown)
class PermissionFlags(enum.Enum):
ReadOnly = 0x1
Hidden = 0x2
System = 0x4
Archive = 0x20
ArchiveHidden = 0x22
ArchiveSystem = 0x24
ArchiveHiddenSystem = 0x26
Device = 0x40
Normal = 0x80
Temporary = 0x100
TempArchive = 0x120
SparseFile = 0x200
ReparsePoint = 0x400
Compressed = 0x800
Offline = 0x1000
NotIndexed = 0x2000
Encrypted = 0x4000
Directory = 0x10000000
IndexView = 0x20000000
unknown = None
@classmethod
def _missing_(cls, value):
return cls(PermissionFlags.unknown)
class MFTEntry(objects.StructType):
"""This represents the base MFT Record"""
def get_signature(self) -> str:
signature = self.Signature.cast('string', max_length = 4, encoding = 'latin-1')
return signature
class MFTFileName(objects.StructType):
"""This represents an MFT $FILE_NAME Attribute"""
def get_full_name(self) -> str:
output = self.Name.cast("string",
encoding = "utf16",
max_length = self.NameLength*2,
errors = "replace")
return output
def get_file_namespace(self) -> str:
pass
@@ -0,0 +1,371 @@
{
"metadata": {
"producer": {
"version": "0.0.1",
"name": "kevthehermit-by-hand",
"comment": "Using structures defined in File System Forensic Analysis pg 353+",
"datetime": "2022-01-03T13:37:00"
},
"format": "6.1.0"
},
"base_types": {
"unsigned long": {
"kind": "int",
"size": 4,
"signed": false,
"endian": "little"
},
"unsigned long long": {
"kind": "int",
"size": 8,
"signed": false,
"endian": "little"
},
"long": {
"kind": "int",
"size": 4,
"signed": true,
"endian": "little"
},
"unsigned int": {
"kind": "int",
"size": 4,
"signed": false,
"endian": "little"
},
"unsigned short": {
"kind": "int",
"size": 2,
"signed": false,
"endian": "little"
},
"unsigned char": {
"kind": "int",
"size": 1,
"signed": false,
"endian": "little"
},
"wchar": {
"kind": "int",
"size": 2,
"signed": true,
"endian": "little"
}
},
"symbols": {},
"enums": {},
"user_types": {
"MFT_ENTRY": {
"fields": {
"Signature": {
"offset": 0,
"type": {
"count": 1,
"kind": "array",
"subtype": {
"kind": "base",
"name": "unsigned char"
}
}
},
"UpdateSequenceOffset": {
"offset": 4,
"type": {
"kind": "base",
"name": "unsigned short"
}
},
"NumFixupEntries": {
"offset": 6,
"type": {
"kind": "base",
"name": "unsigned short"
}
},
"LSN": {
"offset": 8,
"type": {
"kind": "base",
"name": "unsigned long long"
}
},
"SequenceValue": {
"offset": 16,
"type": {
"kind": "base",
"name": "unsigned short"
}
},
"LinkCount": {
"offset": 18,
"type": {
"kind": "base",
"name": "unsigned short"
}
},
"FirstAttrOffset": {
"offset": 20,
"type":{
"kind": "base",
"name": "unsigned short"
}
},
"Flags": {
"offset": 22,
"type":{
"kind": "base",
"name": "unsigned short"
}
},
"RealSize": {
"offset": 24,
"type":{
"kind": "base",
"name": "unsigned int"
}
},
"AlocatedSize": {
"offset": 28,
"type":{
"kind": "base",
"name": "unsigned int"
}
},
"BaseReference": {
"offset": 32,
"type":{
"kind": "base",
"name": "unsigned long long"
}
},
"NextAttrID": {
"offset": 40,
"type":{
"kind": "base",
"name": "unsigned short"
}
},
"RecordNumber": {
"offset": 44,
"type":{
"kind": "base",
"name": "unsigned long"
}
}
},
"kind": "struct",
"size": 1024
},"ATTR_HEADER": {
"fields": {
"AttrType": {
"offset": 0,
"type": {
"kind": "base",
"name": "unsigned int"
}
},"Length": {
"offset": 4,
"type": {
"kind": "base",
"name": "unsigned int"
}
},
"NonResidentFlag": {
"offset": 8,
"type": {
"kind": "base",
"name": "unsigned char" }
},
"NameLength": {
"offset": 9,
"type": {
"kind": "base",
"name": "unsigned char"
}
},
"NameOffset": {
"offset": 10,
"type": {
"kind": "base",
"name": "unsigned short"
}
},
"Flags": {
"offset": 12,
"type": {
"kind": "base",
"name": "unsigned short"
}
},
"AttributeID": {
"offset": 14,
"type": {
"kind": "base",
"name": "unsigned short"
}
}
},
"kind": "struct",
"size": 16
},"RESIDENT_HEADER": {
"fields": {
"AttrSize": {
"offset": 0,
"type": {
"kind": "base",
"name": "unsigned int"
}
},"AttrOffset": {
"offset": 4,
"type": {
"kind": "base",
"name": "unsigned int"
}
},
"IndexFlag": {
"offset": 8,
"type": {
"kind": "base",
"name": "unsigned short" }
}
},
"kind": "struct",
"size": 8
},
"STANDARD_INFORMATION_ENTRY": {
"fields": {
"CreationTime": {
"offset": 0,
"type": {
"kind": "base",
"name": "unsigned long long"
}
},
"ModifiedTime": {
"offset": 8,
"type": {
"kind": "base",
"name": "unsigned long long"
}
},
"UpdatedTime": {
"offset": 16,
"type": {
"kind": "base",
"name": "unsigned long long"
}
},
"AccessedTime": {
"offset": 24,
"type": {
"kind": "base",
"name": "unsigned long long"
}
},
"flags": {
"offset": 32,
"type": {
"kind": "base",
"name": "unsigned short"
}
}
},
"kind": "struct",
"size": 1024
},
"FILE_NAME_ENTRY": {
"fields": {
"ParentDirectory": {
"offset": 0,
"type": {
"kind": "base",
"name": "unsigned long long"
}
},
"CreationTime": {
"offset": 8,
"type": {
"kind": "base",
"name": "unsigned long long"
}
},
"ModifiedTime": {
"offset": 16,
"type": {
"kind": "base",
"name": "unsigned long long"
}
},
"UpdatedTime": {
"offset": 24,
"type": {
"kind": "base",
"name": "unsigned long long"
}
},
"AccessedTime": {
"offset": 32,
"type": {
"kind": "base",
"name": "unsigned long long"
}
},
"AllocatedFileSize": {
"offset": 40,
"type": {
"kind": "base",
"name": "unsigned long long"
}
},
"RealFileSize": {
"offset": 48,
"type": {
"kind": "base",
"name": "unsigned long long"
}
},
"Flags": {
"offset": 56,
"type": {
"kind": "base",
"name": "unsigned int"
}
},
"ReparseValue": {
"offset": 60,
"type": {
"kind": "base",
"name": "unsigned int"
}
},
"NameLength": {
"offset": 64,
"type": {
"kind": "base",
"name": "unsigned char"
}
},
"NameSpace": {
"offset": 65,
"type": {
"kind": "base",
"name": "unsigned char"
}
},
"Name": {
"offset": 66,
"type": {
"count": 10,
"kind": "array",
"subtype": {
"kind": "base",
"name": "wchar"
}
}
}
},
"kind": "struct",
"size": 1024
}
}
}
@@ -0,0 +1,15 @@
# 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
#
from volatility3.framework.symbols import intermed
from volatility3.framework.symbols.windows.extensions import mft
class MFTIntermedSymbols(intermed.IntermediateSymbolTable):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.set_type_class('FILE_NAME_ENTRY', mft.MFTFileName)
self.set_type_class('MFT_ENTRY', mft.MFTEntry)