Windows.Forensics.Prefetch

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!).


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