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
0000ffffor 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
ProgramIdare about the same application. - The
ProgramIdis stable across hosts for the same application install — Office 365 on host A and the same Office 365 build on host B share aProgramId. - The
ProgramIdis not stable across major version upgrades — upgrading the application typically changes itsProgramId.
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, KeyLastWriteTimestampYou 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 WhenThis 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.exe → svchost64.exe → update.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#
- Amcache complete reference — the high-level overview.
- Amcache registry structure —
where
ProgramIdsits in the hive. - Amcache FileId explained — the
content-hash identifier that complements
ProgramId. - Amcache timestamps explained
— how to time-bound your
ProgramIdpivots. - Lateral movement and Amcache
ProgramIdpivoting — the full cross-host hunt playbook.
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
- Volatility and Amcache: extracting the hive from memory images
A practical guide to recovering Amcache from a Windows memory image using Volatility — when memory-side recovery is the only option, which plugins to use, and how to hand off to AmcacheParser.
- RegRipper amcache plugin: what it does and when to use it
A practical guide to RegRipper's amcache plugin — what it parses, how its text output differs from AmcacheParser's CSV, and when to reach for it instead of (or alongside) the Zimmerman tool.
- What is Amcache ProgramId? (glossary)
ProgramId is the 44-character application-identity hash Amcache assigns to each logical application. The same ProgramId on different hosts means the same application install.
- AmcacheParser output columns explained: every CSV field decoded
A field-by-field reference for AmcacheParser's CSV output — FileId, PathHash, ProgramId, LinkDate, BinFileVersion, IsPeFile, and every other column, with the pivots that matter in DFIR.