505 lines
18 KiB
Python
505 lines
18 KiB
Python
#!/usr/bin/python
|
|
# -*- coding: utf-8 -*-
|
|
#
|
|
# Copyright 2013 The Plaso Project Authors.
|
|
# Please see the AUTHORS file for details on individual authors.
|
|
#
|
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
# you may not use this file except in compliance with the License.
|
|
# You may obtain a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
# See the License for the specific language governing permissions and
|
|
# limitations under the License.
|
|
"""Parser for Windows Prefetch files."""
|
|
|
|
import logging
|
|
import os
|
|
|
|
import construct
|
|
|
|
from plaso.events import time_events
|
|
from plaso.events import windows_events
|
|
from plaso.lib import binary
|
|
from plaso.lib import errors
|
|
from plaso.lib import eventdata
|
|
from plaso.parsers import interface
|
|
from plaso.parsers import manager
|
|
|
|
|
|
class WinPrefetchExecutionEvent(time_events.FiletimeEvent):
|
|
"""Class that defines a Windows Prefetch execution event."""
|
|
|
|
DATA_TYPE = 'windows:prefetch:execution'
|
|
|
|
def __init__(
|
|
self, timestamp, timestamp_description, file_header, file_information,
|
|
mapped_files, path, volume_serial_numbers, volume_device_paths):
|
|
"""Initializes the event.
|
|
|
|
Args:
|
|
timestamp: The FILETIME timestamp value.
|
|
timestamp_description: The usage string for the timestamp value.
|
|
file_header: The file header construct object.
|
|
file_information: The file information construct object.
|
|
mapped_files: A list of the mapped filenames.
|
|
path: A path to the executable.
|
|
volume_serial_numbers: A list of volume serial number strings.
|
|
volume_device_paths: A list of volume device path strings.
|
|
"""
|
|
super(WinPrefetchExecutionEvent, self).__init__(
|
|
timestamp, timestamp_description)
|
|
|
|
self.offset = 0
|
|
|
|
self.version = file_header.get('version', None)
|
|
self.executable = binary.Ut16StreamCopyToString(
|
|
file_header.get('executable', ''))
|
|
self.prefetch_hash = file_header.get('prefetch_hash', None)
|
|
|
|
self.run_count = file_information.get('run_count', None)
|
|
self.mapped_files = mapped_files
|
|
self.path = path
|
|
|
|
self.number_of_volumes = file_information.get('number_of_volumes', 0)
|
|
self.volume_serial_numbers = volume_serial_numbers
|
|
self.volume_device_paths = volume_device_paths
|
|
|
|
|
|
class WinPrefetchParser(interface.BaseParser):
|
|
"""A parser for Windows Prefetch files."""
|
|
|
|
NAME = 'prefetch'
|
|
DESCRIPTION = u'Parser for Windows Prefetch files.'
|
|
|
|
FILE_SIGNATURE = 'SCCA'
|
|
|
|
FILE_HEADER_STRUCT = construct.Struct(
|
|
'file_header',
|
|
construct.ULInt32('version'),
|
|
construct.String('signature', 4),
|
|
construct.Padding(4),
|
|
construct.ULInt32('file_size'),
|
|
construct.String('executable', 60),
|
|
construct.ULInt32('prefetch_hash'),
|
|
construct.ULInt32('flags'))
|
|
|
|
FILE_INFORMATION_V17 = construct.Struct(
|
|
'file_information_v17',
|
|
construct.ULInt32('metrics_array_offset'),
|
|
construct.ULInt32('number_of_metrics_array_entries'),
|
|
construct.ULInt32('trace_chains_array_offset'),
|
|
construct.ULInt32('number_of_trace_chains_array_entries'),
|
|
construct.ULInt32('filename_strings_offset'),
|
|
construct.ULInt32('filename_strings_size'),
|
|
construct.ULInt32('volumes_information_offset'),
|
|
construct.ULInt32('number_of_volumes'),
|
|
construct.ULInt32('volumes_information_size'),
|
|
construct.ULInt64('last_run_time'),
|
|
construct.Padding(16),
|
|
construct.ULInt32('run_count'),
|
|
construct.Padding(4))
|
|
|
|
FILE_INFORMATION_V23 = construct.Struct(
|
|
'file_information_v23',
|
|
construct.ULInt32('metrics_array_offset'),
|
|
construct.ULInt32('number_of_metrics_array_entries'),
|
|
construct.ULInt32('trace_chains_array_offset'),
|
|
construct.ULInt32('number_of_trace_chains_array_entries'),
|
|
construct.ULInt32('filename_strings_offset'),
|
|
construct.ULInt32('filename_strings_size'),
|
|
construct.ULInt32('volumes_information_offset'),
|
|
construct.ULInt32('number_of_volumes'),
|
|
construct.ULInt32('volumes_information_size'),
|
|
construct.Padding(8),
|
|
construct.ULInt64('last_run_time'),
|
|
construct.Padding(16),
|
|
construct.ULInt32('run_count'),
|
|
construct.Padding(84))
|
|
|
|
FILE_INFORMATION_V26 = construct.Struct(
|
|
'file_information_v26',
|
|
construct.ULInt32('metrics_array_offset'),
|
|
construct.ULInt32('number_of_metrics_array_entries'),
|
|
construct.ULInt32('trace_chains_array_offset'),
|
|
construct.ULInt32('number_of_trace_chains_array_entries'),
|
|
construct.ULInt32('filename_strings_offset'),
|
|
construct.ULInt32('filename_strings_size'),
|
|
construct.ULInt32('volumes_information_offset'),
|
|
construct.ULInt32('number_of_volumes'),
|
|
construct.ULInt32('volumes_information_size'),
|
|
construct.Padding(8),
|
|
construct.ULInt64('last_run_time'),
|
|
construct.ULInt64('last_run_time1'),
|
|
construct.ULInt64('last_run_time2'),
|
|
construct.ULInt64('last_run_time3'),
|
|
construct.ULInt64('last_run_time4'),
|
|
construct.ULInt64('last_run_time5'),
|
|
construct.ULInt64('last_run_time6'),
|
|
construct.ULInt64('last_run_time7'),
|
|
construct.Padding(16),
|
|
construct.ULInt32('run_count'),
|
|
construct.Padding(96))
|
|
|
|
METRICS_ARRAY_ENTRY_V17 = construct.Struct(
|
|
'metrics_array_entry_v17',
|
|
construct.ULInt32('start_time'),
|
|
construct.ULInt32('duration'),
|
|
construct.ULInt32('filename_string_offset'),
|
|
construct.ULInt32('filename_string_number_of_characters'),
|
|
construct.Padding(4))
|
|
|
|
# Note that at the moment for the purpose of this parser
|
|
# the v23 and v26 metrics array entry structures are the same.
|
|
METRICS_ARRAY_ENTRY_V23 = construct.Struct(
|
|
'metrics_array_entry_v23',
|
|
construct.ULInt32('start_time'),
|
|
construct.ULInt32('duration'),
|
|
construct.ULInt32('average_duration'),
|
|
construct.ULInt32('filename_string_offset'),
|
|
construct.ULInt32('filename_string_number_of_characters'),
|
|
construct.Padding(4),
|
|
construct.ULInt64('file_reference'))
|
|
|
|
VOLUME_INFORMATION_V17 = construct.Struct(
|
|
'volume_information_v17',
|
|
construct.ULInt32('device_path_offset'),
|
|
construct.ULInt32('device_path_number_of_characters'),
|
|
construct.ULInt64('creation_time'),
|
|
construct.ULInt32('serial_number'),
|
|
construct.Padding(8),
|
|
construct.ULInt32('directory_strings_offset'),
|
|
construct.ULInt32('number_of_directory_strings'),
|
|
construct.Padding(4))
|
|
|
|
# Note that at the moment for the purpose of this parser
|
|
# the v23 and v26 volume information structures are the same.
|
|
VOLUME_INFORMATION_V23 = construct.Struct(
|
|
'volume_information_v23',
|
|
construct.ULInt32('device_path_offset'),
|
|
construct.ULInt32('device_path_number_of_characters'),
|
|
construct.ULInt64('creation_time'),
|
|
construct.ULInt32('serial_number'),
|
|
construct.Padding(8),
|
|
construct.ULInt32('directory_strings_offset'),
|
|
construct.ULInt32('number_of_directory_strings'),
|
|
construct.Padding(68))
|
|
|
|
def _ParseFileHeader(self, file_object):
|
|
"""Parses the file header.
|
|
|
|
Args:
|
|
file_object: A file-like object to read data from.
|
|
|
|
Returns:
|
|
The file header construct object.
|
|
"""
|
|
try:
|
|
file_header = self.FILE_HEADER_STRUCT.parse_stream(file_object)
|
|
except (IOError, construct.FieldError) as exception:
|
|
raise errors.UnableToParseFile(
|
|
u'Unable to parse file header with error: {0:s}'.format(exception))
|
|
|
|
if not file_header:
|
|
raise errors.UnableToParseFile(u'Unable to read file header')
|
|
|
|
if file_header.get('signature', None) != self.FILE_SIGNATURE:
|
|
raise errors.UnableToParseFile(u'Unsupported file signature')
|
|
|
|
return file_header
|
|
|
|
def _ParseFileInformation(self, file_object, format_version):
|
|
"""Parses the file information.
|
|
|
|
Args:
|
|
file_object: A file-like object to read data from.
|
|
format_version: The format version.
|
|
|
|
Returns:
|
|
The file information construct object.
|
|
"""
|
|
try:
|
|
if format_version == 17:
|
|
file_information = self.FILE_INFORMATION_V17.parse_stream(file_object)
|
|
elif format_version == 23:
|
|
file_information = self.FILE_INFORMATION_V23.parse_stream(file_object)
|
|
elif format_version == 26:
|
|
file_information = self.FILE_INFORMATION_V26.parse_stream(file_object)
|
|
else:
|
|
file_information = None
|
|
except (IOError, construct.FieldError) as exception:
|
|
raise errors.UnableToParseFile(
|
|
u'Unable to parse v{0:d} file information with error: {1:s}'.format(
|
|
format_version, exception))
|
|
|
|
if not file_information:
|
|
raise errors.UnableToParseFile(
|
|
u'Unable to read v{0:d} file information'.format(format_version))
|
|
return file_information
|
|
|
|
def _ParseMetricsArray(self, file_object, format_version, file_information):
|
|
"""Parses the metrics array.
|
|
|
|
Args:
|
|
file_object: A file-like object to read data from.
|
|
format_version: The format version.
|
|
file_information: The file information construct object.
|
|
|
|
Returns:
|
|
A list of metrics array entry construct objects.
|
|
"""
|
|
metrics_array = []
|
|
|
|
metrics_array_offset = file_information.get('metrics_array_offset', 0)
|
|
number_of_metrics_array_entries = file_information.get(
|
|
'number_of_metrics_array_entries', 0)
|
|
|
|
if metrics_array_offset > 0 and number_of_metrics_array_entries > 0:
|
|
file_object.seek(metrics_array_offset, os.SEEK_SET)
|
|
|
|
for entry_index in range(0, number_of_metrics_array_entries):
|
|
try:
|
|
if format_version == 17:
|
|
metrics_array_entry = self.METRICS_ARRAY_ENTRY_V17.parse_stream(
|
|
file_object)
|
|
elif format_version in [23, 26]:
|
|
metrics_array_entry = self.METRICS_ARRAY_ENTRY_V23.parse_stream(
|
|
file_object)
|
|
else:
|
|
metrics_array_entry = None
|
|
except (IOError, construct.FieldError) as exception:
|
|
raise errors.UnableToParseFile((
|
|
u'Unable to parse v{0:d} metrics array entry: {1:d} with error: '
|
|
u'{2:s}').format(format_version, entry_index, exception))
|
|
|
|
if not metrics_array_entry:
|
|
raise errors.UnableToParseFile(
|
|
u'Unable to read v{0:d} metrics array entry: {1:d}'.format(
|
|
format_version, entry_index))
|
|
|
|
metrics_array.append(metrics_array_entry)
|
|
|
|
return metrics_array
|
|
|
|
def _ParseFilenameStrings(self, file_object, file_information):
|
|
"""Parses the filename strings.
|
|
|
|
Args:
|
|
file_object: A file-like object to read data from.
|
|
file_information: The file information construct object.
|
|
|
|
Returns:
|
|
A dict of filename strings with their byte offset as the key.
|
|
"""
|
|
filename_strings_offset = file_information.get('filename_strings_offset', 0)
|
|
filename_strings_size = file_information.get('filename_strings_size', 0)
|
|
|
|
if filename_strings_offset > 0 and filename_strings_size > 0:
|
|
file_object.seek(filename_strings_offset, os.SEEK_SET)
|
|
filename_strings_data = file_object.read(filename_strings_size)
|
|
filename_strings = binary.ArrayOfUt16StreamCopyToStringTable(
|
|
filename_strings_data)
|
|
|
|
else:
|
|
filename_strings = {}
|
|
|
|
return filename_strings
|
|
|
|
def _ParseVolumesInformationSection(
|
|
self, file_object, format_version, file_information):
|
|
"""Parses the volumes information section.
|
|
|
|
Args:
|
|
file_object: A file-like object to read data from.
|
|
format_version: The format version.
|
|
file_information: The file information construct object.
|
|
|
|
Yields:
|
|
A volume information construct object.
|
|
"""
|
|
volumes_information_offset = file_information.get(
|
|
'volumes_information_offset', 0)
|
|
|
|
if volumes_information_offset > 0:
|
|
number_of_volumes = file_information.get('number_of_volumes', 0)
|
|
file_object.seek(volumes_information_offset, os.SEEK_SET)
|
|
|
|
while number_of_volumes > 0:
|
|
try:
|
|
if format_version == 17:
|
|
yield self.VOLUME_INFORMATION_V17.parse_stream(file_object)
|
|
else:
|
|
yield self.VOLUME_INFORMATION_V23.parse_stream(file_object)
|
|
except (IOError, construct.FieldError) as exception:
|
|
raise errors.UnableToParseFile((
|
|
u'Unable to parse v{0:d} volume information with error: '
|
|
u'{1:s}').format(format_version, exception))
|
|
|
|
number_of_volumes -= 1
|
|
|
|
def _ParseVolumeDevicePath(
|
|
self, file_object, file_information, volume_information):
|
|
"""Parses the volume device path.
|
|
|
|
This function expects the current offset of the file-like object to point
|
|
as the end of the volume information structure.
|
|
|
|
Args:
|
|
file_object: A file-like object to read data from.
|
|
file_information: The file information construct object.
|
|
volume_information: The volume information construct object.
|
|
|
|
Returns:
|
|
A Unicode string containing the device path or None if not available.
|
|
"""
|
|
volumes_information_offset = file_information.get(
|
|
'volumes_information_offset', 0)
|
|
|
|
device_path = None
|
|
if volumes_information_offset > 0:
|
|
device_path_offset = volume_information.get('device_path_offset', 0)
|
|
device_path_size = 2 * volume_information.get(
|
|
'device_path_number_of_characters', 0)
|
|
|
|
if device_path_offset >= 36 and device_path_size > 0:
|
|
device_path_offset += volumes_information_offset
|
|
current_offset = file_object.tell()
|
|
|
|
file_object.seek(device_path_offset, os.SEEK_SET)
|
|
device_path = binary.ReadUtf16Stream(
|
|
file_object, byte_size=device_path_size)
|
|
|
|
file_object.seek(current_offset, os.SEEK_SET)
|
|
|
|
return device_path
|
|
|
|
def Parse(self, parser_context, file_entry, parser_chain=None):
|
|
"""Extracts events from a Windows Prefetch file.
|
|
|
|
Args:
|
|
parser_context: A parser context object (instance of ParserContext).
|
|
file_entry: A file entry object (instance of dfvfs.FileEntry).
|
|
parser_chain: Optional string containing the parsing chain up to this
|
|
point. The default is None.
|
|
|
|
Raises:
|
|
UnableToParseFile: when the file cannot be parsed.
|
|
"""
|
|
file_object = file_entry.GetFileObject()
|
|
file_header = self._ParseFileHeader(file_object)
|
|
|
|
format_version = file_header.get('version', None)
|
|
if format_version not in [17, 23, 26]:
|
|
raise errors.UnableToParseFile(
|
|
u'Unsupported format version: {0:d}'.format(format_version))
|
|
|
|
# Add ourselves to the parser chain, which will be used in all subsequent
|
|
# event creation in this parser.
|
|
parser_chain = self._BuildParserChain(parser_chain)
|
|
|
|
file_information = self._ParseFileInformation(file_object, format_version)
|
|
metrics_array = self._ParseMetricsArray(
|
|
file_object, format_version, file_information)
|
|
try:
|
|
filename_strings = self._ParseFilenameStrings(
|
|
file_object, file_information)
|
|
except UnicodeDecodeError as exception:
|
|
logging.warning((
|
|
u'[{0:s}] Unable to parse filename information from file {1:s} '
|
|
u'with error: {2:s}').format(
|
|
parser_chain,
|
|
file_entry.path_spec.comparable.replace(u'\n', u';'),
|
|
exception))
|
|
filename_strings = {}
|
|
|
|
if len(metrics_array) != len(filename_strings):
|
|
logging.debug(
|
|
u'Mismatch in number of metrics and filename strings array entries.')
|
|
|
|
executable = binary.Ut16StreamCopyToString(
|
|
file_header.get('executable', u''))
|
|
|
|
volume_serial_numbers = []
|
|
volume_device_paths = []
|
|
path = u''
|
|
|
|
for volume_information in self._ParseVolumesInformationSection(
|
|
file_object, format_version, file_information):
|
|
volume_serial_number = volume_information.get('serial_number', 0)
|
|
volume_device_path = self._ParseVolumeDevicePath(
|
|
file_object, file_information, volume_information)
|
|
|
|
volume_serial_numbers.append(volume_serial_number)
|
|
volume_device_paths.append(volume_device_path)
|
|
|
|
timestamp = volume_information.get('creation_time', 0)
|
|
if timestamp:
|
|
event_object = windows_events.WindowsVolumeCreationEvent(
|
|
timestamp, volume_device_path, volume_serial_number,
|
|
file_entry.name)
|
|
parser_context.ProduceEvent(
|
|
event_object, parser_chain=parser_chain, file_entry=file_entry)
|
|
|
|
for filename in filename_strings.itervalues():
|
|
if not filename:
|
|
continue
|
|
if (filename.startswith(volume_device_path) and
|
|
filename.endswith(executable)):
|
|
_, _, path = filename.partition(volume_device_path)
|
|
|
|
mapped_files = []
|
|
for metrics_array_entry in metrics_array:
|
|
file_reference = metrics_array_entry.get('file_reference', 0)
|
|
filename_string_offset = metrics_array_entry.get(
|
|
'filename_string_offset', 0)
|
|
|
|
filename = filename_strings.get(filename_string_offset, u'')
|
|
if not filename:
|
|
logging.debug(u'Missing filename string for offset: {0:d}.'.format(
|
|
filename_string_offset))
|
|
continue
|
|
|
|
if file_reference:
|
|
mapped_file_string = (
|
|
u'{0:s} [MFT entry: {1:d}, sequence: {2:d}]').format(
|
|
filename, file_reference & 0xffffffffffffL,
|
|
file_reference >> 48)
|
|
else:
|
|
mapped_file_string = filename
|
|
|
|
mapped_files.append(mapped_file_string)
|
|
|
|
timestamp = file_information.get('last_run_time', 0)
|
|
if timestamp:
|
|
event_object = WinPrefetchExecutionEvent(
|
|
timestamp, eventdata.EventTimestamp.LAST_RUNTIME, file_header,
|
|
file_information, mapped_files, path, volume_serial_numbers,
|
|
volume_device_paths)
|
|
parser_context.ProduceEvent(
|
|
event_object, parser_chain=parser_chain, file_entry=file_entry)
|
|
|
|
# Check for the 7 older last run time values available in v26.
|
|
if format_version == 26:
|
|
for last_run_time_index in range(1, 8):
|
|
last_run_time_identifier = 'last_run_time{0:d}'.format(
|
|
last_run_time_index)
|
|
|
|
timestamp = file_information.get(last_run_time_identifier, 0)
|
|
if timestamp:
|
|
event_object = WinPrefetchExecutionEvent(
|
|
timestamp,
|
|
u'Previous {0:s}'.format(eventdata.EventTimestamp.LAST_RUNTIME),
|
|
file_header, file_information, mapped_files, path,
|
|
volume_serial_numbers, volume_device_paths)
|
|
parser_context.ProduceEvent(
|
|
event_object, parser_chain=parser_chain, file_entry=file_entry)
|
|
|
|
file_object.close()
|
|
|
|
|
|
manager.ParsersManager.RegisterParser(WinPrefetchParser)
|