Windows keeps a cache of prefetch files. When an executable is run, the system records properties about the executable to make it faster to run next time. By parsing this information we are able to determine when binaries are run in the past. On Windows10 we can see the last 8 execution times and creation time (9 potential executions).
There are several parameter’s available for this artifact.
NOTE: The Prefetch file format is described extensively in libscca and painstakingly reversed by Joachim Metz (Shouts and Thank you!).
name: Windows.Forensics.Prefetch
description: |
Windows keeps a cache of prefetch files. When an executable is run,
the system records properties about the executable to make it faster
to run next time. By parsing this information we are able to
determine when binaries are run in the past. On Windows10 we can see
the last 8 execution times and creation time (9 potential executions).
There are several parameter's available for this artifact.
- dateAfter enables search for prefetch evidence after this date.
- dateBefore enables search for prefetch evidence before this date.
- binaryRegex enables to filter on binary name, e.g evil.exe.
- hashRegex enables to filter on prefetch hash.
NOTE: The Prefetch file format is described extensively in libscca
and painstakingly reversed by Joachim Metz (Shouts and Thank you!).
reference:
- https://www.forensicswiki.org/wiki/Prefetch
- https://github.com/libyal/libscca/blob/main/documentation/Windows%20Prefetch%20File%20(PF)%20format.asciidoc
parameters:
- name: prefetchGlobs
default: C:\Windows\Prefetch\*.pf
- name: dateAfter
description: "search for events after this date. YYYY-MM-DDTmm:hh:ssZ"
type: timestamp
- name: dateBefore
description: "search for events before this date. YYYY-MM-DDTmm:hh:ssZ"
type: timestamp
- name: binaryRegex
description: "Regex of executable name."
type: regex
- name: hashRegex
description: "Regex of prefetch hash."
type: regex
- name: IncludeFilesAccessed
description: Include all accessed files
type: bool
export: |
LET PrefetchProfile = '''[
["Header", 8, [
["Signature", 0, "String", {"length": 3}],
["UncompressedSize", 4, "unsigned long"],
["Data", 8, String, {
length: "x=>x.UncompressedSize",
term: "",
max_length: 10000000,
}],
["Decompressed", 0, "Value", {
value: "x=>lzxpress_decompress(data=x.Data)"
}],
]],
["SCCAHeader", 84, [
["Version", 0, "Enumeration", {
type: "unsigned int",
choices: {
"17": "WinXP (17)",
"23": "Vista (23)",
"26": "Win8.1 (26)",
"30": "Win10 (30)",
"31": "Win11 (31)"
}
}],
["Signature", 4, "String", {"length": 4}],
["FileSize", 12, "unsigned long"],
["Executable", 16, "String", {
encoding: "utf16",
}],
["Hash", 76, "unsigned long"],
# Hash is followed by a version specific info struct.
["Info", 84, "Union", {
selector: "x=>x.Version",
choices: {
"WinXP (17)": "FileInformationWinXP",
"Vista (23)": "FileInformationVista",
"Win8.1 (26)": "FileInformationWin81",
"Win10 (30)": "FileInformationWin10",
"Win11 (31)": "FileInformationWin10"
}
}]
]],
["FileInformationWinXP", 68, [
["__FileMetricsOffset", 0, "unsigned long"],
["__NumberOfFileMetrics", 4, "unsigned long"],
["__TraceChainsArrayOffset", 8, "unsigned long"],
["__NumberOfTraceChains", 12, "unsigned long"],
["__FilenameOffset", 16, "unsigned long"],
["__FilenameSize", 20, "unsigned long"],
["__VolumesInformationOffset", 24, "unsigned long"],
["__NumberOfVolumes", 28, "unsigned long"],
["__VolumesInformationSize", 32, "unsigned long"],
# This is realy just one time but we make it an
# array to be compatible with the others.
["LastRunTimes", 36, "Array", {
"type": "Timestamp",
"count": 1
}],
["RunCount", 60, "unsigned long"],
# Metrics offset is absolute.
["Metrics", "x=>x.__FileMetricsOffset - x.StartOf", "Array", {
type: "FileMetricsEntryV17",
count: "x=>x.__NumberOfFileMetrics",
}],
["VolumeInfo", "x=>x.__VolumesInformationOffset - x.StartOf", "Array", {
type: "VolumeInformation",
count: "x=>x.__NumberOfVolumes",
}],
]],
["FileInformationVista", 156, [
["__FileMetricsOffset", 0, "unsigned long"],
["__NumberOfFileMetrics", 4, "unsigned long"],
["__TraceChainsArrayOffset", 8, "unsigned long"],
["__NumberOfTraceChains", 12, "unsigned long"],
["__FilenameOffset", 16, "unsigned long"],
["__FilenameSize", 20, "unsigned long"],
["__VolumesInformationOffset", 24, "unsigned long"],
["__NumberOfVolumes", 28, "unsigned long"],
["__VolumesInformationSize", 32, "unsigned long"],
# This is realy just one time but we make it an
# array to be compatible with the others.
["LastRunTimes", 44, "Array", {
"type": "Timestamp",
"count": 1
}],
["RunCount", 68, "unsigned long"],
# Metrics offset is absolute.
["Metrics", "x=>x.__FileMetricsOffset - x.StartOf", "Array", {
type: "FileMetricsEntryV23",
count: "x=>x.__NumberOfFileMetrics",
}],
["VolumeInfo", "x=>x.__VolumesInformationOffset - x.StartOf", "Array", {
type: "VolumeInformation",
count: "x=>x.__NumberOfVolumes",
}],
]],
["FileInformationWin81", 224, [
["__FileMetricsOffset", 0, "unsigned long"],
["__NumberOfFileMetrics", 4, "unsigned long"],
["__TraceChainsArrayOffset", 8, "unsigned long"],
["__NumberOfTraceChains", 12, "unsigned long"],
["__FilenameOffset", 16, "unsigned long"],
["__FilenameSize", 20, "unsigned long"],
["__VolumesInformationOffset", 24, "unsigned long"],
["__NumberOfVolumes", 28, "unsigned long"],
["__VolumesInformationSize", 32, "unsigned long"],
# This is realy just one time but we make it an
# array to be compatible with the others.
["LastRunTimes", 44, "Array", {
"type": "Timestamp",
"count": 8,
}],
["RunCount", 124, "unsigned long"],
# Metrics offset is absolute.
["Metrics", "x=>x.__FileMetricsOffset - x.StartOf", "Array", {
type: "FileMetricsEntryV23",
count: "x=>x.__NumberOfFileMetrics",
}],
["VolumeInfo", "x=>x.__VolumesInformationOffset - x.StartOf", "Array", {
type: "VolumeInformation",
count: "x=>x.__NumberOfVolumes",
}],
]],
["FileInformationWin10", 224, [
["__FileMetricsOffset", 0, "unsigned long"],
["__NumberOfFileMetrics", 4, "unsigned long"],
["__TraceChainsArrayOffset", 8, "unsigned long"],
["__NumberOfTraceChains", 12, "unsigned long"],
["__FilenameOffset", 16, "unsigned long"],
["__FilenameSize", 20, "unsigned long"],
["__VolumesInformationOffset", 24, "unsigned long"],
["__NumberOfVolumes", 28, "unsigned long"],
["__VolumesInformationSize", 32, "unsigned long"],
["__TotalDirectoryCount", 36, "unsigned long"],
["LastRunTimes", 44, "Array", {
"type": "Timestamp",
"count": 8
}],
["__RunCount1", 124, "unsigned long"],
["__RunCountPre", 120, "unsigned long"],
["__RunCount2", 116, "unsigned long"],
["RunCount", 0, Value, {
value: "x=>if(condition=x.__RunCountPre=0, then=x.__RunCount1, else=x.__RunCount2)",
}],
# Metrics offset is absolute.
["Metrics", "x=>x.__FileMetricsOffset - x.StartOf", "Array", {
type: "FileMetricsEntryV30",
count: "x=>x.__NumberOfFileMetrics",
}],
["VolumeInfo", "x=>x.__VolumesInformationOffset - x.StartOf", "Array", {
type: "VolumeInformation",
count: "x=>x.__NumberOfVolumes",
}],
]],
["Timestamp", 8, [
["Date", 0, "WinFileTime"],
["Int", 0, "unsigned long long"]
]],
["FileMetricsEntryV17", 20, [
["__FilenameOffset", 8, "unsigned long"],
["__FilenameLength", 12, "unsigned long"],
["Filename", 0, "Profile", {
offset: "x=>x.ParentOf.__FilenameOffset + x.__FilenameOffset",
type: "String",
type_options: {
encoding: "utf16",
length: 1024,
}
}]
]],
["FileMetricsEntryV23", 32, [
["__FilenameOffset", 12, "unsigned long"],
["__FilenameLength", 16, "unsigned long"],
["__MFTFileReference", 24, "unsigned long"],
["Filename", 0, "Profile", {
offset: "x=>x.ParentOf.__FilenameOffset + x.__FilenameOffset",
type: "String",
type_options: {
encoding: "utf16",
length: 1024,
}
}]
]],
["FileMetricsEntryV30", 32, [
["__FilenameOffset", 12, "unsigned long"],
["__FilenameLength", 16, "unsigned long"],
["__MFTFileReference", 24, "unsigned long"],
["Filename", 0, "Profile", {
offset: "x=>x.ParentOf.__FilenameOffset + x.__FilenameOffset",
type: "String",
type_options: {
encoding: "utf16",
length: 1024,
}
}]
]],
["VolumeInformation", 40, [
["__DeviceOffset", 0, "unsigned long"],
["DeviceName", "x=>x.__DeviceOffset", "String", {
encoding: utf16,
length: "x=>x.__DeviceSize * 2",
}],
["__DeviceSize", 4, "unsigned long"],
["DeviceCreationTime", 8, "WinFileTime"],
["VolumeSerialNumber", 12, "unsigned long"],
["VolumeSerialNumberHex", 0, Value, {
value: "x=>format(format='%#x', args=x.VolumeSerialNumber)",
}],
["__FileReferenceOffset", 20, "unsigned long"],
["__FileReferenceDataSize", 24, "unsigned long"],
["__DirectoryStringsOffset", 28, "unsigned long"],
["__NumDirectoryStrings", 32, "unsigned long"],
["__Directories", "x=>x.__DirectoryStringsOffset", "Array", {
type: "DirectoryName",
count: "x=>x.__NumDirectoryStrings",
}],
["Directories", 0, Value, {
value: "x=>x.__Directories.Name"
}],
]],
["DirectoryName", "x=>x.Size * 2 + 4", [
["Size", 0, "uint8"],
["Name", 2, "String", {
encoding: "utf16",
length: "x=>x.Size * 2"
}]
]]
]
'''
LET ParsePrefetch(PrefetchFile) = SELECT
parse_binary(accessor="data", filename=Data,
profile=PrefetchProfile, struct="SCCAHeader") AS SCCAHeader
FROM switch(a={
-- Handle compressed MAM prefetch files.
SELECT
parse_binary(filename=PrefetchFile, profile=PrefetchProfile, struct="Header") AS Header,
parse_binary(filename=PrefetchFile, profile=PrefetchProfile, struct="Header").Decompressed AS Data
FROM scope()
WHERE Header.Signature = "MAM"
},
b={
-- Handle uncompressed files
SELECT read_file(filename=PrefetchFile, length=1024*1024) AS Data
FROM scope()
})
WHERE SCCAHeader.Signature = "SCCA"
sources:
- query: |
// Parse prefetch files and apply non time filters
LET pf = SELECT * FROM foreach(
row={
SELECT * FROM glob(globs=prefetchGlobs)
},
query={
SELECT SCCAHeader AS _SCCAHeader,
SCCAHeader.Executable AS Executable,
SCCAHeader.FileSize AS FileSize,
format(format="%#X", args=SCCAHeader.Hash) AS Hash,
SCCAHeader.Version AS Version,
filter(list=SCCAHeader.Info.LastRunTimes.Date, condition="x=>x.Unix > 0") AS LastRunTimes,
SCCAHeader.Info.RunCount AS RunCount,
OSPath,
Name AS PrefetchFileName,
Btime as CreationTime,
Mtime as ModificationTime,
filter(list=SCCAHeader.Info.Metrics.Filename, regex=".exe$")[0] AS Binary,
if(condition= IncludeFilesAccessed, then=SCCAHeader.Info.Metrics.Filename) AS FilesAccessed,
if(condition= IncludeFilesAccessed, then=SCCAHeader.Info.VolumeInfo) AS VolumeInfo
FROM ParsePrefetch(PrefetchFile=OSPath)
WHERE
if(condition=binaryRegex, then= Executable =~ binaryRegex, else=TRUE) AND
if(condition=hashRegex, then= Hash =~ hashRegex, else=TRUE)
})
// Flattern to enable time filters. Remember VQL is lazy.
LET executionTimes = SELECT * FROM flatten(
query = {
SELECT *,
OSPath as FilteredPath,
LastRunTimes as ExecutionTime
FROM pf
})
WHERE
if(condition=dateAfter, then=ExecutionTime > timestamp(string=dateAfter),
else=TRUE) AND
if(condition=dateBefore, then=ExecutionTime < timestamp(string=dateBefore),
else=TRUE)
LET creationTimes = SELECT * FROM flatten(
query = {
SELECT *,
OSPath as FilteredPath,
CreationTime as ExecutionTime
FROM pf
WHERE RunCount > 8
})
WHERE
if(condition=dateAfter, then=ExecutionTime > timestamp(string=dateAfter),
else=TRUE) AND
if(condition=dateBefore, then=ExecutionTime < timestamp(string=dateBefore),
else=TRUE)
GROUP BY ExecutionTime
// For stdOutput with timefilters we need to group by OSPath
LET timeFiltered = SELECT FilteredPath
FROM chain(
a = { SELECT * FROM executionTimes },
b = { SELECT * FROM creationTimes })
GROUP BY FilteredPath
LET timeFilteredStdOut = SELECT * FROM foreach(
row={
SELECT * FROM timeFiltered
},
query={
SELECT *
FROM pf
WHERE OSPath = FilteredPath
})
SELECT *
FROM if(condition = (dateBefore OR dateAfter),
then={ SELECT * FROM timeFilteredStdOut },
else={ SELECT * FROM pf })
column_types:
- name: CreationTime
type: timestamp
- name: ModificationTime
type: timestamp