"Did the program run?" is the simplest question in a Windows investigation and the one analysts answer wrong most often. The temptation is to find a .pf file with the right name and call it. That is correct in the majority of cases. It is dangerous in the minority, and the minority is exactly where contested investigations live.
This post is about the limits of the Prefetch proof-of-execution claim. What it really says. What it does not say. And how to firm it up with the artifacts that survive next to it.
The claim, written precisely
When the Prefetch service observes a process starting and gets through the first ten seconds of its execution, it writes (or updates) a .pf file. The filename embeds the executable's binary name (uppercased, truncated to 29 characters) and a hash derived from the executable's full path.
So the claim a .pf file supports, written carefully, is:
A process was started on this host whose primary executable had the name
EXAMPLE.EXEand whose resolved path produced hash0x1234ABCD, at the times recorded in the file's execution timestamp array, and was observed by the Prefetch service for long enough to be recorded.
Everything outside that sentence is inference, not direct evidence.
That is still a stronger claim than what you get from Shimcache ("Windows looked at this binary's metadata at some point") or AmCache ("Windows enumerated this binary for compatibility purposes, which usually but not always implies execution"). Prefetch requires the process to actually start. That is hard to fake by accident.
How the path hash works, and where it lies
The hash in the filename is not a content hash. It is a function of the executable's full path (with some additional context fields on newer Windows builds, like command-line arguments and package identifiers for UWP apps). Two binaries with the same name in two different directories produce two different .pf files. That is the property that lets you tell C:\Program Files\PsExec.exe apart from C:\Users\victim\AppData\Local\Temp\PsExec.exe at a glance.
But the path is the only input, not the binary. If C:\Users\victim\AppData\Local\Temp\PsExec.exe ran today, and yesterday a totally different file lived at that path (also named PsExec.exe), they share a .pf file. The execution timestamps stack together. The run counter increments. The load list overwrites with the most recent run's data.
This is the failure mode I have seen the most. An attacker drops a tool, runs it, deletes it, and later a benign file ends up at the same path. The original Prefetch record is still there, but the path on disk now points at something else entirely. An analyst who only looks at the path and the file currently on disk concludes that the benign file ran. The benign file did not run. Something else with the same path did.
The mitigation is to never trust path resolution alone. Pair the .pf with the MFT entry for that path at the time of execution, recover the file reference number, and check whether the file ID has changed since.
Filename collisions and the 29-character problem
The internal executable name field is 29 characters plus a null terminator, UTF-16LE. Binary names longer than that get truncated.
If you have ever seen two .pf files with names like:
LONGNAMEDEXECUTABLE_VERSION_A-12345678.pf
LONGNAMEDEXECUTABLE_VERSION_B-87654321.pf
and both internal name fields read LONGNAMEDEXECUTABLE_VERSION_, you have a collision in the name but distinct hashes. The hashes are what disambiguate.
Conversely, the hash space is wide but not infinite, and the hash is a 32-bit value. Hash collisions across paths do happen. They are rare in the wild but not unheard of, and on a busy host with thousands of binaries you should not be shocked to see two .pf files with the same hash bytes if the binary names differ. The combination of name and hash is the unique key, not either one alone.
The "executable replaced after .pf was created" edge case
Here is the version of this case that has actually walked into my engagements.
Day 0, an attacker copies m1m1.exe (renamed Mimikatz) to C:\Users\Public\m1m1.exe. Runs it. Windows creates M1M1.EXE-AABBCCDD.pf. The load list shows \Windows\System32\sekurlsa.dll and other artifacts that scream Mimikatz.
Day 3, the attacker deletes the binary.
Day 7, an internal user, totally unrelated, writes a tiny utility, names it m1m1.exe, drops it in C:\Users\Public\, runs it. Windows updates the existing .pf file (same path, same hash). The load list now reflects the benign utility. The run count is now 2. The most recent execution time is day 7.
A casual reading says "binary ran twice, latest run looks benign". The correct reading needs additional artifacts. The MFT will show that the file at C:\Users\Public\m1m1.exe was created day 0, deleted day 3, and recreated day 7 with a different file reference. The USN journal will have both lifecycles. AmCache may have the hash of the day-0 binary even after the file is deleted. Sysmon Event ID 1 will have the command line and SHA-256 of both executions if it was running.
Prefetch alone says "ran twice". Prefetch plus the surrounding artifacts says "two different binaries with the same path ran, here are the hashes, here is which user, here is what they did".
If you stop at Prefetch alone in a case like this, you will be wrong in the most embarrassing way: confidently.
Pair with EVTX 4688 and Sysmon EID 1
The two events you want next to every Prefetch execution claim are Security 4688 (process creation, if enabled) and Sysmon Event ID 1 (process creation, with command line, parent, hashes, integrity level). These are in the EVTX.
Match on:
- Executable path. The 4688/EID 1
NewProcessNameorImagefield should match the resolved path Prefetch recorded. - Execution time. The Prefetch
Last runtime and the event time should agree to within seconds. Larger gaps are not necessarily wrong but are worth investigating; the most common cause is clock skew between different log sources. - Process integrity level and user context. Prefetch is per-machine and tells you nothing about who ran the binary. Sysmon and 4688 give you the SID. If a binary's
.pfshows ten executions and Sysmon only logs five, you are missing logs, or the other five happened in a window where Sysmon was off, or somebody tampered with the event log.
The pair is much stronger than either alone. Prefetch gives you a complete inventory of every binary that ran (subject to the SSD/server caveats covered elsewhere); EVTX gives you the per-execution detail. Use them together and the "did this run" question stops being ambiguous.
When you have neither, RAM gives you a live process list if the host has not rebooted, and the pagefile sometimes has process artifacts. After a reboot with no EVTX and no Prefetch, you are mostly down to AmCache and inference.
Telling "ran once" from "ran in the noise"
The Prefetch run counter does not lie about the lower bound. If it says 12 executions, the binary ran at least 12 times. Whether it ran more (because some executions aged out of the eight-timestamp ring or because Win10 1709+ trimmed older entries) is open.
What the run counter is good for in this context is contrast. On a host with one psexec.exe .pf showing 200 executions and a path inside Program Files, you are looking at a sysadmin's daily tool. On the same host with a separate psexec.exe .pf showing 1 execution and a path under \AppData\Local\Temp\, you are looking at something that arrived recently and ran once. The second .pf is the one to investigate.
Counter-intuitively, the single-execution Prefetch files are usually the interesting ones in an intrusion. Many-execution Prefetch files are usually background noise unless they live in suspicious places.
A workflow that holds up
When the question is "did this run on this host", I run the following sequence:
- Identify the
.pffile by name and hash. - Read its execution timestamps and run counter. Note the executable path it recorded.
- Pull the MFT entry for that path and check that the file reference matches what Prefetch saw.
- Cross-reference with EVTX 4688 and Sysmon EID 1 on the run times.
- Check AmCache for the file hash. If it matches a known sample, the case becomes simpler.
- If the binary is no longer on disk, check the USN journal for delete events around the run times.
If steps 3 through 6 all agree, the execution claim is essentially unimpeachable. If they disagree, you have something more interesting than a simple proof of execution; you have evidence of tampering, replacement, or a more complex sequence than the Prefetch entry alone suggests.
Further reading
- Eric Zimmerman, PECmd — the parser. Read the release notes for what changed across Windows builds.
- Andrea Fortuna, Forensic analysis of Prefetch files — clear write-up with examples.
- Microsoft, Audit Process Creation (event 4688) — the partner artifact, including the command-line-in-4688 audit policy that most estates have not turned on.
Prefetch answers "the binary ran". Sysmon answers "this is what it did". Treat them as complementary and you will not get caught short.