Registry Parser
All articles

File-association defaults and hijacks: the FileExts UserChoice key

7 min read

The FileExts UserChoice key is where Windows records, per user, which application opens each file extension. It is a small artifact with two jobs in an investigation: it tells you what handler a given file type points at, and — when that pointer has been bent — it surfaces a file association hijack, the quiet trick where a benign extension is repointed at an attacker's binary so a routine double-click runs their code. The default app registry data for the current user lives entirely in NTUSER.DAT, which means it carries the one thing system-wide handler config does not: attribution to a specific user profile.

The trap to flag up front: people reach for HKCR (the system-wide class registrations) and stop there. The effective default on a modern box is the per-user UserChoice under HKCU, and it wins over the system default. If you read the machine-wide associations and call it done, you have read the fallback, not the answer.

Where it lives

The per-user associations sit under the user's NTUSER.DAT at:

Software\Microsoft\Windows\CurrentVersion\Explorer\FileExts\<.ext>

(HKCU\...\Explorer\FileExts\<.ext> when you are looking at a live system.) Under each extension subkey — .pdf, .html, .txt, .js, and so on — you typically find three things:

  • UserChoice — a subkey holding the chosen default handler. The value that matters here is ProgId: a string naming the ProgId (the registered application class) that opens this extension for this user. There is also a Hash value, covered below.
  • OpenWithProgids — a subkey listing the ProgIds Windows knows can open this extension. These are the candidates that populate the "Open with" menu; they are not the default. Multiple entries are normal.
  • OpenWithList — a subkey recording executables surfaced in the "Open with" dialog, usually with an MRUList ordering value pointing at a, b, c entries. This is the legacy, executable-name-based list as opposed to the ProgId-based one.

The artifact this site's parser extracts is the first and most load-bearing of those: for every extension subkey under FileExts, it reads UserChoice\ProgId and reports the pair. The output is two columns — Extension and Default app (ProgId) — one row per extension that actually has a UserChoice\ProgId set. Extensions with no UserChoice are skipped, because there is no per-user override to report; for those, the system default in HKCR still governs.

So a clean result looks like a flat mapping:

Extension   Default app (ProgId)
.pdf        Acrobat.Document.DC
.html       ChromeHTML
.txt        txtfile
.jpg        PhotoViewer.FileAssoc.Jpeg

Each row is "this user chose this ProgId to open this extension." The ProgId is the indirection: it is not the path to the binary. To get from ChromeHTML to chrome.exe, you resolve the ProgId's shell\open\command under HKCR\<ProgId> (or the user's Software\Classes\<ProgId>). That second hop is where the actual command line — and therefore the actual handler — lives.

What it records, and what it does not

UserChoice\ProgId records the default handler the user (or something acting as the user) selected. OpenWithProgids and OpenWithList record the menu of alternatives, including handlers the user picked once via "Open with" without making them the default. That distinction matters in triage: a malicious ProgId sitting in OpenWithProgids is a candidate the user could pick; a malicious ProgId sitting in UserChoice is the one that fires on a plain double-click, with no menu, no prompt. The latter is the finding.

What this key does not tell you is when the handler ran, or how many times. It is a configuration artifact, not an execution log. It states the current intent — "this is what opens .pdf for jdoe right now." For execution evidence you pivot to UserAssist and the rest of the execution-artifact set. FileExts answers "what would open this file type," not "what did, on Tuesday."

The UserChoice Hash, briefly and defensively

Each UserChoice subkey carries a Hash value alongside ProgId. This is not decoration. Windows 8 introduced it, and Windows 10 hardened it, specifically to stop programs from silently rewriting default associations behind the user's back — the "set yourself as the default browser without asking" behaviour that plagued earlier Windows. The Hash is a value derived from the extension, the ProgId, the user's SID, and other inputs, computed by an algorithm Microsoft did not publish. If you write a new ProgId into UserChoice without a matching, correctly-computed Hash, Windows detects the mismatch and ignores — or resets — the association.

The forensic reading of this, framed defensively: the hash raises the bar for programmatic tampering but does not make hijacking impossible. The algorithm has been reverse-engineered publicly more than once, so a sufficiently motivated attacker can forge a valid hash and write a UserChoice that Windows will honour. More commonly, malware sidesteps the whole problem by driving the legitimate UI (synthesizing the "How do you want to open this?" interaction) or by attacking OpenWithProgids and the ProgId's command rather than UserChoice itself. Two practical consequences for you:

  • A UserChoice\ProgId you do not trust, paired with a Hash that Windows is currently honouring, means the change was made through a path Windows accepted — interactively, or with a forged-but-valid hash. Either way it is effective, and effective is what matters for triage.
  • Do not treat the presence of the hash as evidence the association is benign. It is an integrity check against accidental clobbering, not an authenticity check against a determined adversary. I would hedge on the exact hardening behaviour per build — the enforcement has shifted across servicing updates — so confirm on the version in front of you rather than assuming a fixed rule.

Spotting a hijacked association

This is MITRE T1546.001, Change Default File Association — and it sits inside the broader hijack-execution-flow family, where the attacker does not start a new process so much as borrow an existing trigger. The mechanism is exactly the indirection described above: repoint an extension's UserChoice\ProgId (or repoint the ProgId's shell\open\command) at something attacker-controlled, then wait for the user to open a file of that type. The double-click is the user's; the code is the attacker's.

The reading discipline, given the parser's flat Extension → ProgId table:

  1. Scan for ProgIds that do not match the extension's normal handler. .txt mapped to txtfile or Notepad++_file is unremarkable. .txt mapped to a ProgId you have never seen, or one whose name mimics a real one, is worth a second look.
  2. Resolve every suspicious ProgId to its command. A normal-looking ProgId can still carry a poisoned shell\open\command — e.g. a command line that launches cmd.exe, wscript.exe, mshta.exe, powershell.exe, or a binary under %APPDATA%/%LOCALAPPDATA%\Temp with the real document path passed as an argument. The ProgId name can be perfectly innocent while the command behind it is not.
  3. Weight common, double-clicked extensions heavily. .pdf, .docx, .html, .jpg, .txt are the ones a user opens without thinking. An attacker hijacking .7z reaches fewer victims than one hijacking .pdf.
  4. Compare against the system default. If UserChoice points somewhere different from the machine-wide HKCR association, that delta is the change someone made for this user. A delta toward a script host or a temp-directory binary is the finding.

The inverse use is just as valuable, and less dramatic: when you need to know what opened a given file type on this profile, FileExts is the authoritative per-user answer. If a malicious .html landed in the user's Downloads and you need to know which browser (and therefore which browser history and cache) to pull, the .html UserChoice ProgId tells you. The artifact cuts both ways — hunt for the hijack, and lean on it for ground truth about handler resolution.

Tools

The parser on this site reads FileExts\<ext>\UserChoice\ProgId for every extension and lays out the Extension → Default app (ProgId) mapping as a persistence-category finding, with the note that a ProgId pointing at an attacker-controlled handler is a defense-evasion / persistence technique (T1546.001). You can analyze NTUSER.DAT in your browser without uploading the hive anywhere.

  • RegRipper has long carried per-user association plugins; its output enumerates the FileExts subkeys and the chosen handlers. Read the plugin source to see exactly which values it pulls — coverage of OpenWithList/OpenWithProgids versus UserChoice alone varies by version. See the RegRipper plugins reference for the full artifact catalog.
  • Eric Zimmerman's RECmd with an associations batch file will pull the FileExts tree into CSV in one pass; from there you diff against a known-good baseline of the same Windows build.

Whatever you use, make sure it resolves the ProgId to its command. A tool that stops at UserChoice\ProgId gives you the pointer; the malicious payload lives one hop further, in the ProgId's shell\open\command. Read both, and weigh the suspicious extensions — the ones a user double-clicks daily — first.