Amcache ProgramId explained: the 44-character application identity

The ProgramId field in Root\InventoryApplicationFile and Root\InventoryApplication is Amcache's logical application identity. A 44-character hex string. Deterministic: the same application on a different host typically gets the same value. That makes it one of the most useful cross-host pivots in the entire Windows DFIR toolkit.

This page is the full reference. What the value is, how Windows constructs it, how to use it on a single host, and how to pivot it across an environment.

For broader context, see the Amcache complete reference. For the surrounding registry layout, see Amcache registry structure.

What it looks like#

A typical ProgramId:

0006fa0b2a9f8a4eb9d7c81e8b1f3c5d3e2a0000ffff

44 hex characters, no prefix, no delimiters. Every entry in InventoryApplicationFile carries one. Every entry in InventoryApplication is keyed by one. The two link via the shared value: a file's ProgramId tells you which installed application it belongs to.

Structure of the 44 characters#

Not a single uniform hash. The value encodes several components:

  • A type / version tag (the leading few characters).
  • A hash of the application's identifying attributes (primarily name, publisher, version, language).
  • A trailing discriminator (often 0000ffff or similar) Windows uses for internal classification.

The exact construction has shifted across Windows builds and is not fully documented by Microsoft. The practical rules:

  • Two records with the same ProgramId describe the same application.
  • ProgramId is stable across hosts for the same install. Office 365 on host A and the same Office 365 build on host B share a ProgramId.
  • ProgramId is not stable across major version upgrades. Upgrading the application typically changes it.

Using ProgramId on a single host#

The headline use is joining a file to its application record.

File → application#

You found a suspicious row in *_UnassociatedFileEntries.csv. Its ProgramId is 0006fa0b2a9f8a4eb9d7c81e8b1f3c5d3e2a0000ffff.

Look up the same ProgramId in *_AssociatedFileEntries.csv and in the parent application catalogue (Root\InventoryApplication\<ProgramId>, or the equivalent CSV your parser emits). You get:

  • The application's display name and publisher.
  • The install date.
  • The install source (sometimes a download URL).
  • The full list of files associated with that application.

This is invaluable when an attacker drops a tool that masquerades as a legitimate application. The filename and metadata might say "AdobeReaderUpdater.exe / Adobe Inc.", but its ProgramId either matches no installed Adobe product (suspicious) or matches a different application than its name suggests (very suspicious).

Application → files#

Reverse pivot, equally useful. You found a suspicious application record in InventoryApplication. Perhaps a portable build of something with empty publisher and an unusual install source. Take its ProgramId and list every file that shares it:

$pid = '0006fa0b2a9f8a4eb9d7c81e8b1f3c5d3e2a0000ffff'
 
Import-Csv .\HOST_amcache_UnassociatedFileEntries.csv |
 Where-Object { $_.ProgramId -eq $pid } |
 Select FullPath, Hash, KeyLastWriteTimestamp
 
Import-Csv .\HOST_amcache_AssociatedFileEntries.csv |
 Where-Object { $_.ProgramId -eq $pid } |
 Select FullPath, Hash, KeyLastWriteTimestamp

You get the full file footprint of that application as the appraiser saw it. Every EXE, DLL, and PE tied back to the application. For attacker tooling, this is often the quickest way to enumerate the full set of dropped binaries from a single observed file.

Cross-host pivoting#

This is where ProgramId shines as a hunt primitive. Because the identity is stable across hosts for the same install, a single suspicious ProgramId on one host becomes a query against every other host's Amcache:

$pid = '0006fa0b2a9f8a4eb9d7c81e8b1f3c5d3e2a0000ffff'
 
Get-ChildItem -Recurse -Filter *_UnassociatedFileEntries.csv |
 ForEach-Object {
  $rows = Import-Csv $_.FullName |
   Where-Object { $_.ProgramId -eq $pid }
  if ($rows) {
   $host = $_.PSChildName.Split('_')[0]
   foreach ($r in $rows) {
    [pscustomobject]@{
     Host = $host
     Path = $r.FullPath
     Hash = $r.Hash
     When = $r.KeyLastWriteTimestamp
    }
   }
  }
 } |
 Sort-Object When

This is the lateral-movement pivot. A suspicious ProgramId on one host becomes "every host that has ever had this application, with paths and times". Tighter signal than hash-only pivots, because ProgramId survives small variations in the binary (re-signing, recompilation with the same name/publisher/version).

For the full lateral-movement playbook, see Lateral movement and Amcache ProgramId pivoting.

When ProgramId is the right pivot#

Same family, multiple builds#

An attacker compiles their tool multiple times with the same name, publisher, and version metadata. Only the binary changes. Hashes differ. ProgramId stays the same. Hashing alone misses the family. ProgramId catches it.

Renamed binaries#

mimikatz.exesvchost64.exeupdate.exe. Filename changes. Embedded PE metadata is the same. If the attacker did not bother scrubbing the version-info resource, ProgramId follows the binary across the renames.

Re-deployed tools across an environment#

The attacker drops the same tool on 20 hosts as they move laterally. Hashes may match, paths usually do not. ProgramId is the most consistent identifier across all 20.

When ProgramId is the wrong pivot#

Tools with rotating metadata#

If the attacker recompiles with different name/publisher/version each time, every build gets a different ProgramId. Fall back to file-content Hash because at least one component of the build is identical.

Living-off-the-land binaries#

net.exe, psexec.exe, certutil.exe. Every host that has run them has the same ProgramId. Matches are meaningless. Pivot on command line (4688 / Sysmon 1) or on the path the LOLBIN was executed from instead.

Truly novel binaries#

A file appearing for the very first time anywhere has a ProgramId no other host shares. Without a corpus to compare against, ProgramId does no work. Hash plus Publisher = '' AND IsPeFile = True AND FullPath under \Users\ is the working filter.

Common confusions#

Two things ProgramId is not.

Not a hash of the binary#

Hash (or FileId) is the content hash. ProgramId is an application-identity hash, computed from metadata. Two completely different binaries with identical metadata share a ProgramId. The same binary recompiled with different metadata gets a different ProgramId.

Not a unique-per-host identifier#

Common mistake: assuming ProgramId identifies a specific install. It does not. If a user installs Notepad++ on five hosts, those five hosts share one ProgramId for Notepad++. To distinguish installs, you need install dates, sources, or paths in addition to ProgramId.

Where ProgramId lives in AmcacheParser#

AmcacheParser exposes ProgramId in every category CSV that has it:

CSV ProgramId use
*_UnassociatedFileEntries.csv The application a file claims to belong to (even if the parent application record is missing).
*_AssociatedFileEntries.csv The application the file is genuinely associated with.
*_ProgramEntries.csv The application's own identity (legacy schema).
*_ShortcutEntries.csv The application the shortcut points at.

For pivots, the standard pattern is: identify a suspicious row in UnassociatedFileEntries, take its ProgramId, run the query patterns above.

Further reading#

  • Yogesh Khatri's Amcache deep dive for the original schema mapping.
  • Maxim Suhanov's writeups on the appraiser's identity computation.

Related posts

Back to all posts