Back to all posts

The list of timestamps in a .pf file is what most analysts read. The list of files the executable touched is what they skim. That ordering is backwards, and I have built more than one investigation out of patterns I found in the load list that the timestamps alone would never have surfaced.

This post is about reading the resources array. What it captures. What it does not. And the three or four shapes that tell you something is off the moment you sort the column the right way.

What the load list actually contains

The Prefetch service watches the first ten seconds of a process's lifetime. Every file the kernel resolves a path for during that window ends up in the file metrics array, with the actual path stored as a UTF-16LE string in Section C. By the time you read the parsed output, those resolved paths look like \DEVICE\HARDDISKVOLUME2\Windows\System32\kernel32.dll and \DEVICE\HARDDISKVOLUME2\Users\bob\Documents\contract.docx.

This is broader than "DLLs loaded". It includes:

  • All DLLs loaded by the binary's process, including delay-loaded ones, as long as the load happens within the trace window.
  • Files opened (in the sense of CreateFile) by the process. Reads in particular, because Windows traces page faults; some writes too, depending on the version.
  • Configuration files, data files, document files, registry hive files if the process opens them directly through the file system rather than the registry API.
  • DLLs and files touched by child processes the binary spawned in its first ten seconds, on some Windows builds.

The cap is around 1024 entries per .pf file. On very chatty binaries you will hit the cap and the rest gets dropped. On most binaries you get the full set.

The trace window is fixed at roughly ten seconds. A binary that lays low for thirty seconds and then loads its real payload will have a misleadingly small load list, because the heavy lifting happened outside the window.

What it does not contain

Writes are partially recorded but not reliably. Files the process created and then never opened again might not be in the list. If you need a complete record of files written, you need Sysmon Event ID 11 in EVTX or the USN journal.

The command line is not in the load list. The arguments passed to the executable are not in any part of the Prefetch file. If the binary read its arguments and then opened files based on them, the resulting file paths show up. The arguments themselves do not.

Network sockets are not in the load list. Prefetch is a file-system artifact.

Children spawned outside the ten-second window are not in the parent's load list, although they get their own .pf files (subject to the usual SSD/server caveats).

Pattern one: DLL search-order hijacking

The classic detection pattern. A trusted binary like winword.exe or mstsc.exe should load its DLLs from \System32\ (and a small number of well-known directories). A .pf whose load list shows the trusted binary loading version.dll or cryptbase.dll from \AppData\Local\Temp\ or \Users\Public\ is a search-order hijack until proven otherwise.

The shapes to look for:

\DEVICE\HARDDISKVOLUME2\WINDOWS\SYSTEM32\KERNEL32.DLL    <- normal
\DEVICE\HARDDISKVOLUME2\USERS\PUBLIC\VERSION.DLL          <- not normal

In the absence of a known-good baseline, the rule is: System DLLs that load from anywhere other than \Windows\System32\ (or \SysWOW64\ for 32-bit processes, or \Program Files\<vendor>\ for the vendor's own redistributed DLLs) deserve a closer look. Some of those will be legitimate side-by-side assemblies. Most will not.

Sort the load list by path prefix, group everything outside \Windows\ and \Program Files\, and look at what is left. On a clean host most of what is left will be the binary's own data files and user documents. On a compromised host the dropped DLLs will be obvious.

Pattern two: malware configuration paths

Commodity malware tends to store its configuration in a fixed location relative to itself or its install directory. Names like config.bin, settings.dat, tox.lic, client.json, and key.dat show up in load lists with depressing regularity. The path is usually under \AppData\Roaming\<some folder>\, \ProgramData\<some folder>\, or directly next to the binary.

Two things this gives you. First, the existence of a configuration file at a specific path at a specific time, even if the file has since been deleted. Second, a name to search the rest of the host for: if \AppData\Roaming\nzhxxk\config.bin is in a .pf load list, you can pivot to the MFT, the USN journal, and the registry to figure out when the configuration was written, modified, and deleted.

I have built timelines where the Prefetch load list was the only thing that told me a config file ever existed. The file was gone, the directory was gone, and the only surviving evidence was the path string baked into a .pf Section C.

Pattern three: document opens

If the question is "did this user open this document", Prefetch can corroborate. A .pf for winword.exe or acrord32.exe whose load list contains \Users\<user>\Documents\<docname> or \Users\<user>\Downloads\<docname> says the application opened that file during one of its tracked runs.

The caveats: the load list does not tell you which run opened which file. If the .pf shows eight execution times and the load list shows three documents, you cannot directly map "document X was opened during run Y". The list is cumulative across the trace window of the most recent runs, with older entries being overwritten as the binary is re-traced.

For per-instance document opens, combine the Prefetch evidence with LNK files, jump lists, and Office's own MRU lists in the registry. The Prefetch entry corroborates that the file path was resolved by the process; the per-user artifacts tell you when and from where the user invoked it.

Pattern four: configuration disclosure from legitimate binaries

This one comes up in insider-threat and intellectual-property cases more than malware ones, but it is worth flagging.

When a process touches a file, that path goes into the load list whether the file is sensitive or not. If a script ran findstr.exe over an HR directory, the .pf for findstr.exe will record the resolved paths of files it actually opened. Same for tar.exe, 7z.exe, xcopy.exe, and every other archive or copy tool.

What this means in practice: when you suspect data exfiltration involving a known utility, pull the .pf for that utility and look at its load list. The files the utility touched will be there. You will not get the command-line arguments, but you will get a partial enumeration of the files involved.

The pattern is also useful for detecting reconnaissance. A .pf for whoami.exe or net.exe is normal on most hosts; a .pf for nltest.exe with \Windows\debug\ paths in the load list is reconnaissance until proven otherwise. The load list tells you what the tool actually queried.

Pattern five: temp directory archaeology

The \AppData\Local\Temp\ and \Windows\Temp\ directories accumulate executables and DLLs that attackers leave behind. Many of these get deleted, sometimes within seconds of being used. The .pf files for the binaries that read them, however, survive.

A Prefetch load list for rundll32.exe showing \AppData\Local\Temp\evil.dll is telling you that DLL was loaded by rundll32.exe, the DLL was at that path at that moment, and (since the .pf for rundll32.exe is updated on each run) some recent execution of rundll32 did exactly that. If the file is no longer there, you have evidence of its prior existence and use.

Combine with the load list's path resolution timestamps (Section A's start/duration fields) and you can sometimes order the loads within the trace window. The granularity is not great but it is enough to tell "loaded immediately on start" from "loaded near the end of the trace window".

Working the load list efficiently

A workflow that I find pays off:

  1. Parse the .pf to a flat CSV with columns: executable, executable path, run count, last run, loaded path. One row per (executable, loaded path) pair.
  2. Filter out paths starting with \Windows\, \Program Files\, \Program Files (x86)\. These are mostly noise on a host where you have not yet identified anomalies.
  3. Sort what remains by executable, then by path. Look at what each binary touched outside the standard directories.
  4. For binaries that touched user-writable directories (\AppData\, \ProgramData\, \Users\Public\, \Temp\), check whether the files are still there. Pair with the MFT.
  5. For interesting hits, pull the corresponding EVTX Sysmon EID 11 (file create) or EID 7 (image load) entries to corroborate.

Step 2 is what makes the load list tractable. A raw load-list dump for a busy host can be hundreds of thousands of lines. Filtering to non-system paths cuts that to something you can actually read.

What the load list is not

The load list is not a process behavior log. It is a snapshot of paths Windows resolved during a short trace window. Treat it as a high-recall, low-precision view of "files this process touched". Some entries will be incidental. Some will be evidence. The skill is knowing which is which.

The good news is that the patterns above hold across most of the binaries and threat actors I have seen in practice. Once you have spent an afternoon looking at load lists across a few dozen Prefetch files from a real intrusion, you will start spotting the anomalies before you can explain why.

Further reading