Hunting commodity malware with Amcache
When you arrive at a Windows host that feels off - user complained, EDR threw a low-confidence alert, firewall log shows behaviour the host should not be exhibiting - Amcache is one of the fastest artefacts to give you a yes/no on "is there obvious attacker tooling here?".
This page is the commodity-malware triage playbook. The specific filters, pivots, and queries that surface the typical attacker artefacts on the typical infected workstation.
For artefact background, see the Amcache complete reference. For combining with other execution evidence, see Amcache vs Prefetch, Amcache vs Shimcache, and Amcache vs SRUM.
Why Amcache is the right first stop#
Three properties:
- SHA-1 of every PE the appraiser saw. That goes straight into a VirusTotal or TI-feed query.
- Full path on every record. Path is half the story for commodity malware. Anything in
\Users\<x>\AppData\Local\Temp\or\ProgramData\is suspect by default. - Survives binary deletion. See Recovering deleted-binary evidence from Amcache.
Together: parse, filter, hash-check, and you have a defensible "is this host owned?" in minutes.
The baseline triage filter#
Apply this to *_UnassociatedFileEntries.csv first, every time:
Import-Csv .\HOST_amcache_UnassociatedFileEntries.csv |
Where-Object {
$_.IsPeFile -eq 'True' -and
-not $_.Publisher -and
$_.FullPath -match '\\Users\\|\\ProgramData\\|\\AppData\\|\\Temp\\|\\Public\\'
} |
Select KeyLastWriteTimestamp, FullPath, Hash, Size, LinkDate |
Sort-Object KeyLastWriteTimestamp -DescendingWhat this surfaces: unsigned PE binaries in paths a normal user can write to.
What it misses (you need follow-up filters for):
- Signed-but-malicious binaries. Signed ransomware, signed RATs. Filter
Publisheris wrong here; pivot on hash against threat intel instead. - Binaries in protected paths (
\Windows\System32\). Required admin to drop, happens with privilege escalation. Different filter. - Living-off-the-land binaries (LOLBINs). Signed and legitimately in
System32. Pivot on command line in 4688 / Sysmon 1 instead.
This filter typically returns 5-50 rows on a typical infected workstation. Most are false positives (portable apps, dev tools, custom scripts). Triage from there.
False-positive controls#
Patterns that filter out obvious benign rows fast.
Exclude developer / portable-app paths#
$benignPaths = @(
'\\AppData\\Local\\JetBrains\\',
'\\AppData\\Local\\Programs\\Microsoft VS Code\\',
'\\AppData\\Local\\Programs\\Slack\\',
'\\AppData\\Local\\Programs\\Notion\\',
'\\AppData\\Roaming\\Spotify\\',
'\\AppData\\Local\\GitHubDesktop\\',
'\\AppData\\Local\\Microsoft\\Teams\\',
'\\AppData\\Local\\Discord\\',
'\\AppData\\Local\\1Password\\'
)
$rows | Where-Object {
$path = $_.FullPath
-not ($benignPaths | Where-Object { $path -match $_ })
}Tune the list to your environment's standard developer / power-user tooling. A baseline of "known portable apps in user paths" is worth maintaining once per environment.
Exclude known publishers via hash#
If you have a known-good hash list (internal allowlist, NSRL, Cisco Talos clean-hash feed), join Amcache Hash against it and drop matches.
Don't waste time on decoy-name filtering#
update.exe, service.exe, svchost.exe in user paths are suspicious because they share names with legitimate Windows binaries. The names alone aren't the signal. Path + missing publisher are. The baseline filter already catches these.
High-precision attacker patterns#
Once you have the filtered list, these patterns are typically high-precision indicators on a typical endpoint.
\AppData\Local\Temp\ + PE + unsigned#
C:\Users\bob\AppData\Local\Temp\xyz1234.tmp.exe
C:\Users\bob\AppData\Local\Temp\setup_temp\install.exe
Almost never a benign developer pattern. Common for:
- Initial-access droppers from phishing attachments.
- Office macro payloads.
- Browser-delivered downloads that execute from
Temp.
Suspicious filenames that mimic OS components#
C:\Users\Public\Documents\svchost.exe
C:\ProgramData\msmpeng.exe
C:\Users\bob\AppData\Roaming\winupdate.exe
OS-component-style names in paths no OS component would ever live at. Near-certain malware indicator.
LinkDate clusters#
Sort the filtered list by LinkDate:
$rows | Sort-Object LinkDateLook for clusters. 3-10 binaries with LinkDate values within a few hours of each other. Attackers frequently compile their full toolkit in one sitting, and link timestamps cluster. Strong "this is one campaign" signal.
Remember LinkDate is attacker-controlled. It can be faked. But when you see a tight cluster of unsigned PEs in user-writable paths with LinkDate from the same afternoon, that is rarely a coincidence.
Hash-only matches in Unassociated#
For each Hash from your filtered rows, check whether it also appears in *_AssociatedFileEntries.csv (matched to an installed application). If the same hash appears Associated on this host or any other collected host, the binary has a benign-or-tracked context. If it appears only Unassociated and only on this one host, that increases suspicion.
Confirming execution#
A row in Amcache means the binary was on disk at appraiser time. It does not mean it ran. To confirm:
Prefetch#
For each suspicious row, check whether C:\Windows\Prefetch\ contains a .pf:
NOTEPAD.EXE-1A2B3C4D.pf
XYZ1234.TMP.EXE-FEDCBA98.pf
A .pf is definitive execution evidence. Parse Prefetch with PECmd.exe to get run timestamps. See Amcache vs Prefetch.
Security 4688 / Sysmon 1#
With process-creation auditing enabled (it should be), every launch logs a 4688 or Sysmon 1 with full command line. Filter to the binary's path and you get every execution with arguments. Usually the most useful single data point in a malware triage.
SRUM#
If the binary used measurable CPU or network, SRUM has an hour-level row. See Amcache vs SRUM. Especially useful for network activity: if SRUM shows the binary sent gigabytes, that's your exfiltration timeline.
Scoping across the estate#
Once you have one confirmed bad binary on one host, the question is where else.
Hash pivot#
$badHashes = @('da39a3ee5e6b4b0d3255bfef95601890afd80709',
'1234567890abcdef1234567890abcdef12345678')
Get-ChildItem -Recurse -Filter *_UnassociatedFileEntries.csv |
ForEach-Object {
$host = $_.PSChildName.Split('_')[0]
Import-Csv $_.FullName |
Where-Object { $badHashes -contains $_.Hash } |
Select @{n='Host';e={$host}}, FullPath, KeyLastWriteTimestamp
} |
Sort-Object KeyLastWriteTimestampEvery collected host that ever had any of the bad hashes, sorted by appraiser-seen time. Often reveals patient zero (earliest) and spread (subsequent timestamps).
ProgramId pivot for tool families#
$badProgramIds = @('0006fa0b2a9f8a4eb9d7c81e8b1f3c5d3e2a0000ffff')
Get-ChildItem -Recurse -Filter *_UnassociatedFileEntries.csv |
ForEach-Object {
$host = $_.PSChildName.Split('_')[0]
Import-Csv $_.FullName |
Where-Object { $badProgramIds -contains $_.ProgramId } |
Select @{n='Host';e={$host}}, FullPath, Hash, KeyLastWriteTimestamp
}Catches family-level matches that Hash misses (rebuilds with same metadata). See Amcache ProgramId explained.
Path-pattern pivot#
Get-ChildItem -Recurse -Filter *_UnassociatedFileEntries.csv |
ForEach-Object {
$host = $_.PSChildName.Split('_')[0]
Import-Csv $_.FullName |
Where-Object {
$_.FullPath -match '\\AppData\\Roaming\\[a-z0-9]{8}\\update\.exe$'
} |
Select @{n='Host';e={$host}}, FullPath, Hash, KeyLastWriteTimestamp
}For known attacker path patterns (a specific install location an intrusion set uses), pivot by path regex.
The full triage flow#
End to end:
- Collect Amcache (
Amcache.hve+.LOG1+.LOG2) from the suspect host. - Parse with
AmcacheParser.exe --mp. - Apply the baseline filter to
*_UnassociatedFileEntries.csv. - Apply false-positive controls (known portable apps, allowlist hashes).
- Triage the remainder. VirusTotal each
Hash. Look at suspicious paths and names. - Confirm execution via Prefetch / 4688 / Sysmon for each confirmed-bad row.
- Time-bound the incident. Take the earliest
KeyLastWriteTimestampand the latest Prefetch run time. That window is your incident timeframe. - Scope the spread via hash /
ProgramIdpivots across all collected Amcache. - Confirm cleanup. Re-run after containment to verify bad rows don't have new corresponding Prefetch runs.
Hours, not days. Works on the vast majority of commodity-malware engagements without specialised EDR.
Further reading#
- The Talos URLhaus and MalwareBazaar feeds for hash enrichment.
- MITRE ATT&CK TA0002 Execution for technique mapping.
- Microsoft Threat Analytics and Eric Zimmerman's tool documentation for adjacent artefacts.
Related#
Related posts
- Volatility and Amcache: extracting the hive from memory images
When you only have a RAM dump, Volatility extracts the in-memory copy of Amcache.hve. Hand off to AmcacheParser. The cases where this is the only option.
- RegRipper amcache plugin: what it does and when to use it
RegRipper's amcache plugin produces a plain-text Amcache report. Useful when you're already running RegRipper across other hives or need narrative output. Use AmcacheParser when you need CSV.
- AmcacheParser output columns explained: every CSV field decoded
Field-by-field reference for AmcacheParser's CSV output. FileId, PathHash, ProgramId, LinkDate, BinFileVersion, IsPeFile, and every other column, with the pivots that matter.
- AmcacheParser download guide: official sources, mirrors, and verification
Where to get AmcacheParser. Get-ZimmermanTools, direct download, KAPE, Velociraptor. Plus checksum verification and the air-gapped install pattern.