Lateral movement and Amcache: ProgramId pivoting across hosts

When you confirm attacker tooling on one Windows host, the next question is always: where else? Lateral movement scoping — identifying every other host the attacker reached — is one of the hardest steps in an investigation, and Amcache is one of the most useful tools for it.

The reason: Amcache stores two cross-host pivots that almost no other Windows artefact stores:

  • Hash — the SHA-1 of the first 31 MiB of every PE the appraiser saw.
  • ProgramId — the 44-character application-identity hash, stable across hosts for the same application.

A single suspicious Hash or ProgramId on Host A becomes a query you can run against the parsed Amcache CSVs of every other host you have collected. This page is the full playbook.

For the prerequisites, see Amcache complete reference, Amcache FileId explained, and Amcache ProgramId explained.


The collection prerequisite#

The pattern works only if you have collected Amcache from many hosts in a way that lets you join across them. Two practical collection patterns:

KAPE-based per-host collection#

Use a single collection root with per-host sub-directories:

\\fileshare\incident-042\
├── HOST01\
│   └── Windows\AppCompat\Programs\Amcache.hve (+ logs)
├── HOST02\
│   └── ...
├── HOST03\
│   ...

Parse with AmcacheParser pointed at each per-host directory:

Get-ChildItem '\\fileshare\incident-042' -Directory | ForEach-Object {
    $host = $_.Name
    AmcacheParser.exe `
      -f "$($_.FullName)\Windows\AppCompat\Programs\Amcache.hve" `
      --csv "\\fileshare\parsed\$host" `
      --csvf "${host}_amcache.csv" `
      --mp
}

You end up with <host>_amcache_UnassociatedFileEntries.csv per host, all in one directory.

Velociraptor fleet hunt#

The Windows.Forensics.Amcache artifact, run as a hunt, deposits per-host parsed output into the Velociraptor server. The CSVs are named with the host's hostname; the same cross-host queries below apply.


The hash pivot#

The simplest and highest-precision pivot. You have a known-bad SHA-1 from Host A; find every other host that has it.

$badHash = 'da39a3ee5e6b4b0d3255bfef95601890afd80709'
 
Get-ChildItem -Recurse -Filter *_UnassociatedFileEntries.csv |
  ForEach-Object {
    $host = $_.PSChildName.Split('_')[0]
    Import-Csv $_.FullName |
      Where-Object { $_.Hash -eq $badHash } |
      Select-Object @{n='Host';e={$host}}, FullPath, KeyLastWriteTimestamp, Size
  } |
  Sort-Object KeyLastWriteTimestamp

Output is a per-host timeline of when the bad binary first appeared on each host. The earliest timestamp is your patient-zero candidate; subsequent timestamps are the spread.

When hash matches over-fit#

The hash pivot misses rebuilds of the same tool. Attackers who recompile their loader before each lateral move have a different hash on every host. For those, fall back to ProgramId.


The ProgramId pivot#

ProgramId is more forgiving than Hash — it catches re-compilations that share name/publisher/version even when the binary content differs. See Amcache ProgramId explained for how the identity is constructed.

$badProgramId = '0006fa0b2a9f8a4eb9d7c81e8b1f3c5d3e2a0000ffff'
 
Get-ChildItem -Recurse -Filter *_UnassociatedFileEntries.csv |
  ForEach-Object {
    $host = $_.PSChildName.Split('_')[0]
    Import-Csv $_.FullName |
      Where-Object { $_.ProgramId -eq $badProgramId } |
      Select-Object @{n='Host';e={$host}}, FullPath, Hash, KeyLastWriteTimestamp
  } |
  Sort-Object KeyLastWriteTimestamp

This finds every host that has any binary identifying as the same application — even with different content hashes. Pair with the hash pivot: hash for high-precision matches, ProgramId for family-level matches.

When ProgramId over-fits#

ProgramId catches false positives if the attacker piggybacks on a legitimate application identity (e.g. recompiling PsExec.exe-named tooling that gets the same ProgramId as genuine PsExec). Pair with Hash to disambiguate; a row matching ProgramId but with a hash that nobody else in your environment has is highly suspicious.


The path-pattern pivot#

For known attacker install-path patterns, regex against FullPath:

$pattern = '\\AppData\\Roaming\\[a-z0-9]{8}\\update\.exe$'
 
Get-ChildItem -Recurse -Filter *_UnassociatedFileEntries.csv |
  ForEach-Object {
    $host = $_.PSChildName.Split('_')[0]
    Import-Csv $_.FullName |
      Where-Object { $_.FullPath -match $pattern } |
      Select-Object @{n='Host';e={$host}}, FullPath, Hash, ProgramId, KeyLastWriteTimestamp
  }

Useful when:

  • You know the intrusion set's install convention.
  • The attacker rotates hashes and metadata but reuses path patterns.
  • You want to find variants that share path style.

Pair the matches with hash and ProgramId from the per-row results to build a richer detection.


Time-series view of spread#

For each pivot, sort the results by KeyLastWriteTimestamp to see the spread over time. A typical pattern:

2026-04-01 14:23 HOST01  -- patient zero, attacker initial access
2026-04-03 09:11 HOST02  -- 2 days later
2026-04-03 11:34 HOST07
2026-04-03 14:55 HOST09
2026-04-04 02:08 HOST15  -- weekend; attacker working overnight
2026-04-04 02:33 HOST22
2026-04-04 02:51 HOST31

Two readings:

  1. The cadence (multiple hosts within hours of each other) is characteristic of automated lateral-movement tooling (PsExec / WMI / SMB-based).
  2. The night-of-overnight burst is the typical attacker pattern: initial access during business hours, then accelerated movement once they have credentials and control.

Use these patterns to time-bound the rest of your evidence collection. Pull Sysmon / Security 4624 / 4688 for each host in its KeyLastWriteTimestamp ± 1 h window — you get the attacker's actual command lines and credential events with high precision.


Cross-pivoting Driver and Device evidence#

The cross-host pattern is not limited to *_UnassociatedFileEntries.csv. For deeper investigations, run the same pivot patterns against:

*_DriverBinaries.csv#

For BYOVD investigations — a vulnerable driver the attacker loaded on one host is almost certainly loaded on the others they reached. Query by driver Hash or DriverName:

$badDriver = 'mhyprot2.sys'
 
Get-ChildItem -Recurse -Filter *_DriverBinaries.csv |
  ForEach-Object {
    $host = $_.PSChildName.Split('_')[0]
    Import-Csv $_.FullName |
      Where-Object { $_.DriverName -eq $badDriver } |
      Select-Object @{n='Host';e={$host}}, DriverName, Service, DriverSigned, KeyLastWriteTimestamp
  }

*_DeviceContainers.csv#

For investigations where the attacker connected hardware (rare in remote attacks, central in insider-threat cases) — query by Manufacturer or FriendlyName:

$suspiciousVendor = 'HakShop'
 
Get-ChildItem -Recurse -Filter *_DeviceContainers.csv |
  ForEach-Object {
    $host = $_.PSChildName.Split('_')[0]
    Import-Csv $_.FullName |
      Where-Object { $_.Manufacturer -match $suspiciousVendor } |
      Select-Object @{n='Host';e={$host}}, FriendlyName, Manufacturer, KeyLastWriteTimestamp
  }

See USB and device history from Amcache for the device-side patterns in detail.


Combining with non-Amcache sources#

The Amcache pivot tells you where the binary was inventoried. To confirm execution and identify the method of movement, correlate with:

  • 4624 (Logon) + 4625 (Failed logon) on the destination hosts — when did the attacker authenticate, and as whom?
  • 4648 (Explicit credential logon) — credentialed lateral movement (PsExec, RDP with passed-in credentials).
  • Sysmon 1 / 4688 (Process create) with parent process — did the attacker process spawn under services.exe (PsExec / remote service), under wmiprvse.exe (WMI), or under explorer.exe (interactive)?
  • Sysmon 3 (Network connect) + Sysmon 22 (DNS query) — outbound C2.
  • Velociraptor / EDR process trees — same data, easier to navigate.

A single Amcache pivot scoping the spread, joined to per-host authentication and process events on the matching hosts, gives you a defensible timeline of: who, when, from where, via what binary, using what credential.


When the pivot misses#

Three situations where the Amcache cross-host pivot underperforms:

The appraiser hasn't run on the destination hosts yet#

If the attacker moved laterally within hours of your collection, the destination hosts' appraisers may not have inventoried the binary yet. Amcache shows them as clean. Re-collect 24–48 hours later; the spread will appear.

The attacker scrubbed Amcache on each host they reached#

Uncommon (mostly because it is noisy and most attackers do not bother) but possible. Use the Volume Shadow Copy recovery workflow in Where Amcache.hve is on disk on each host where you suspect scrubbing.

The attacker used different binaries per host#

If the attacker generated per-host implants (truly per-victim malware), hash and ProgramId pivots fail by design. Path patterns and broader behavioural detections (4624 logon from the same unusual IP across many hosts) become primary.


See also#

To explore your own collected hives without installing anything, drop a file on the parser home page — it parses entirely in your browser.

Related posts

Back to all posts