Amcache ProgramId explained: the 44-character application identity

The ProgramId field in Root\InventoryApplicationFile and Root\InventoryApplication is Amcache's logical application identity. It is a 44-character hex string that uniquely identifies an application — and, crucially, it is deterministic: the same application on a different host typically gets the same ProgramId. 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 in a single-host investigation, and how to pivot it across an environment in a hunt.

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


What the value looks like#

A typical ProgramId from a real hive:

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#

The value is not a single uniform hash. It encodes several components, in rough form:

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

The exact construction has shifted across Windows builds and is not fully documented by Microsoft. For DFIR purposes the practical rules are:

  • Two records with the same ProgramId are about the same application.
  • The ProgramId is stable across hosts for the same application install — Office 365 on host A and the same Office 365 build on host B share a ProgramId.
  • The ProgramId is not stable across major version upgrades — upgrading the application typically changes its ProgramId.

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 (the registry's Root\InventoryApplication\<ProgramId> key, or the equivalent CSV if your parser emits one). You typically 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 file's 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#

The reverse pivot is equally useful. You found a suspicious application record in InventoryApplication — perhaps a portable build of something with a publisher of "" 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-Object FullPath, Hash, KeyLastWriteTimestamp
 
Import-Csv .\HOST_amcache_AssociatedFileEntries.csv |
  Where-Object { $_.ProgramId -eq $pid } |
  Select-Object FullPath, Hash, KeyLastWriteTimestamp

You get the full file footprint of that application as the appraiser saw it — every EXE, DLL, and PE the appraiser tied back to that 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 with ProgramId#

This is where ProgramId shines as a hunt primitive. Because the identity is stable across hosts for the same application, a single suspicious ProgramId on one host becomes a query you can run 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. If a single host has a suspicious ProgramId, running this query across your collected Amcache CSVs tells you every other host the same application ever appeared on, with paths and times — typically a tighter signal than hash-only pivots because the 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 most useful#

A few investigation patterns where ProgramId is the right pivot:

Same family, multiple builds#

An attacker may compile their tool multiple times with the same name, publisher, and version metadata — only the binary changes. The hashes differ. The ProgramId is the same. Hashing alone misses the family; ProgramId catches it.

Renamed binaries#

mimikatz.exesvchost64.exeupdate.exe. The filename changes; the embedded PE metadata is the same. If the attacker did not bother to scrub the version-info resource, the ProgramId stays the same.

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. The ProgramId is the most consistent identifier across all 20 hosts.


When ProgramId is the wrong pivot#

A few cases where ProgramId underperforms:

Tools with rotating metadata#

If the attacker recompiles with different name/publisher/version each time, every build gets a different ProgramId. For these, file-content hash (the SHA-1 in Hash) is the better cross-host pivot, because at least one component of the build is identical.

Living-off-the-land binaries#

net.exe, psexec.exe, certutil.exe — abused legitimate tools. Every host that has run any of these has the same ProgramId for them. ProgramId matches are essentially meaningless here. 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 that 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 triage 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, not from the binary's bytes. Two completely different binaries with identical metadata get the same ProgramId; the same binary recompiled with different metadata gets a different ProgramId.

Not a unique-per-host identifier#

A common mistake is to assume 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's output#

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, and run the query patterns above.


See also#

To explore ProgramId values on your own hive without installing anything, drop a file on the parser home page — it never leaves your browser.

Related posts

Back to all posts