13 minutes
How Windows UCPD Grew from a 29 KB Registry Filter into a 196 KB Policy Engine
TL;DR
- I collected 38
UCPD.sysbinaries representing 24 versions - UCPD first shipped in August 2023, seven months before its public discovery
- It grew from a 29 KB browser-association filter into a 196 KB registry and process policy engine
- Later versions added ACL, rename, injection, UI Automation, backtrace, and dynamic-rule defenses
- Versions 4.4–4.7 added cross-device resume, gaming,
FeatureV3, and an expanded deny list
Going backward
My recent article examined how the Dynamic Rules (DR) work in the UCPD driver, and earlier research by Christoph Kolbicz, Gunnar Haslinger, and me captured individual stages of its development. The remaining piece was to track how the driver evolved over time.
So I used Winbindex to locate and retrieve 38 reconstructable x64 UCPD.sys files from Microsoft’s public symbol server. Microsoft had removed the corresponding PDBs for most versions, so the comparison relied on the binaries rather than symbol information. The files cover 24 versions, and I compared adjacent versions.
Most of this work was performed by Codex using Binary Ninja’s MCP integration. I was quite surprised by its ability to diff binaries without specialized tools such as BinDiff and BinExport.
UCPD was already in Windows in August 2023
The oldest indexed binary is UCPD.sys version 1.0.1.383513. Winbindex associates it with Windows 11 22H2 preview update KB5029351, released on August 22, 2023.
That is about seven months before the reports that led to UCPD’s public discovery.
The distinction is important. Early 2024 appears to be when Microsoft activated or broadly rolled out the protection, not when it wrote the driver. Microsoft had already been developing and servicing UCPD in public Windows packages for months.
The first version was not an empty shell, either. It was a 29,184-byte driver with 58 unwind-described functions. Its strings and code identify the essential design:
Software\Microsoft\Windows\Shell\Associations\UrlAssociations\http\UserChoice
Software\Microsoft\Windows\Shell\Associations\UrlAssociations\https\UserChoice
RemoveUserChoice
RenameUserChoice
OriginalFilename
*\dllhost.exe
*\reg.exe
*\rundll32.exe
*\svchost.exe
In other words, version 1.0 already knew which registry keys represented the default browser, distinguished operations against them, and had a list of Microsoft utilities that should not be trusted merely because Microsoft signed them.
The architecture that would define UCPD was present from the beginning:
registry operation
|
v
is this a protected key?
|
v
is the requesting executable trusted?
|
+-- not Microsoft-signed -> deny
|
`-- Microsoft-signed
|
`-- utility deny list -> deny
Everything that followed made the protected surface wider and made the definition of “trusted” much harder to fool.
The complete growth curve
The driver grew almost sevenfold over three years:
| Version | Approximate period | Size | Functions | Important visible addition |
|---|---|---|---|---|
| 1.0.1 | Aug 2023 | 29 KB | 58 | HTTP/HTTPS UserChoice protection |
| 2.0.0 | Dec 2023 | 34 KB | 65 | PDF, DeviceRegion, FeatureV2 |
| 2.1.0 | 2024 | 45 KB | 74 | Feeds, NandI, taskbar |
| 2.2.0 | Jun 2024 | 41 KB | 74 | cmd.exe deny entry |
| 3.0.0 | Jul 2024 | 86 KB | 143 | Process/publisher defenses, Search, Taskband |
| 3.1–3.2 | late 2024 | 86–96 KB | 146–156 | Copilot experiments, more denied utilities |
| 4.0.x | early 2025 | 102–112 KB | 181 | Backtrace-era process/module architecture |
| 4.1.x | Mar–Apr 2025 | 115–119 KB | 193 | Unknown/OpenWith, Office, ACL operations |
| 4.2.x | Apr 2025 | 135 KB | 248 | Dynamic rules and self-protection |
| 4.3.0 | Jun 2025 | 157 KB | 295 | StackTrace and renamed-file parsing |
| 4.4.x | late 2025 | 164–174 KB | 312–316 | CrossDeviceResume |
| 4.5.0 | Oct 2025 | 168 KB | 333 | GamingConfiguration |
| 4.6.0 | Feb 2026 | 176 KB | 341 | FeatureV3 and SHA-512 |
| 4.7.x | 2026 | 197 KB | 354–355 | Complete 14-entry deny list |
Function counts come from the x64 exception directory. They are not a perfect source-level function count, but they are consistent enough to show the scale of each transition.
Windows servicing branches do not form a perfectly linear history. The same logical version can appear as multiple binaries for 22H2, 24H2, 25H2, or 26H1, and release dates can cross. I therefore treated file version and policy contents as the primary ordering and used KB dates as supporting provenance.
Version 2: no longer just the default browser
Version 2.0 added the first major expansion:
Software\Microsoft\Windows\CurrentVersion\Explorer\FileExts\.pdf\UserChoice
SOFTWARE\Microsoft\Windows\CurrentVersion\Control Panel\DeviceRegion
FeatureV2
It also introduced UserChoiceLatest and UserChoicePrevious forms for HTTP, HTTPS, and PDF. UserChoiceLatest would not become operationally important until much later, when Windows enabled its new association-hash rotation design. Christoph eventually documented that migration in UserChoiceLatest – Microsoft’s new protection for file type associations.
The code had been waiting in UCPD since 2023.
The deny list also changed. Version 2 removed svchost.exe and added powershell.exe and regedit.exe. Version 2.1 added wscript.exe and cscript.exe; version 2.2 added cmd.exe.
Version 2.1 was also where UCPD visibly escaped its name. It added registry rules for Windows UI features:
SOFTWARE\Microsoft\Windows\CurrentVersion\Feeds
SOFTWARE\Microsoft\Windows\CurrentVersion\NandI
Software\Microsoft\Windows\CurrentVersion\Explorer\Advanced
ShellFeedsTaskbarViewMode
IsFeedsAvailable
TaskbarDa
At that point, “User Choice Protection Driver” was already becoming a misleading description. It was protecting choices, but they were no longer limited to file associations.
Version 3: defending trusted processes from other processes
The jump from 2.2 to 3.0 is the first enormous one: 41 KB became 86 KB and 74 functions became 143.
This is where the problem changed from “who made this registry call?” to “can I trust what is running inside the process that made it?”
Version 3 added process and publisher vocabulary for Kingsoft, Qihoo/360, Edge, Explorer, and Opera. It classified selected processes and used object callbacks to prevent particular vendors from injecting into trusted Microsoft processes or driving the Settings UI through desktop journal playback.
The driver also expanded its registry surface to Search and Taskband, while the deny list gained InfDefaultInstall.exe and pwsh.exe.
Version 3.1 is an interesting reminder that this history is not monotonic. One 3.1 build contains the Copilot BrandedKeyChoiceType rule; another removes its path and value strings. Version 4.0 restores them. That looks like branch divergence or A/B feature experimentation rather than a feature being implemented once and permanently enabled.
The manager confirms the feature-driven design. I recovered the original UCPDMgr input from my old Binary Ninja database, then downloaded its matching PDB from Microsoft’s symbol server. Its feature traits include:
UCPD
UCPDV2
UCPD_PRONG1
UCPD_PRONG2
UCPD_TASKBAR
UCPD_WSB
UCPD_ANTIUIA
UCPD_ANTIINJECTION
UCPD_NEWDENYLIST
UCPD_RENAME_ATTACK
UCPDMgr did not invent the policies. It translated Windows Feature Staging decisions into the FeatureV2 mask consumed by the driver.
Version 4.1: a general registry integrity engine
Version 4.0 broadened the publisher and regional policies, but 4.1 is the more important conceptual transition. It added:
- unknown-file/OpenWith handler repair
.htmand.htmlassociations- Word, Excel, and PowerPoint associations
UserChoiceLatest\ProgIdrules- registry security and ACL operations
- explicit delete and rename handling for the new families
The Unknown handler is especially illustrative. Windows expects these keys to launch the real OpenWith.exe:
SOFTWARE\Classes\Unknown\shell
SOFTWARE\Classes\Unknown\shell\open\command
SOFTWARE\Classes\Unknown\shell\openas\command
UCPD contains the expected command, DelegateExecute CLSID, and dedicated UnknownModify, UnknownDelKey, and UnknownRenKey paths. This is not about choosing a browser. It is registry integrity protection against replacing a trusted shell action with an attacker-controlled command.
Version 4.1 also removed the remaining UserChoicePrevious strings. Windows kept UserChoiceLatest; the earlier fallback design apparently did not survive.
Version 4.2: a signed policy channel appears
Version 4.2 grew from 119 KB to 135 KB and added the machinery that led to my dynamic-rules article:
\REGISTRY\MACHINE\SYSTEM\CurrentControlSet\Services\UCPD\DR
Base64Decode
ParsePEFormat
CalculatePEHashInMem
CertificateVerify
DecryptData
DispatcherConfig
AntiInjection
AllowListV1
DenyListV1
The registry value is a Base64-encoded, data-only PE. UCPD parses it, verifies its Microsoft signature, decrypts the .rdata, and dispatches nested TLV records into independent policy structures.
This solved an operational problem for Microsoft. Before this mechanism, changing UCPD policy required a new driver and usually a reboot. A signed registry policy can change deny/allow, injection, UI Automation, or stack-trace rules independently.
The same version also contains explicit paths for protecting the UCPD service and DR key themselves. A remotely updatable kernel policy is only useful if another administrator-level process cannot replace its input.
Version 4.3: trust the original file and the entire stack
Version 4.3 added two names that summarize the next defenses:
ParseOriginalFileName
StackTrace
The original UCPD trust rule had a wonderfully simple weakness: copy a Microsoft binary under another name. Its signature remained valid, but it no longer matched the utility deny list. VMware even used this approach for its default-association tooling.
The OriginalFilename check closes that gap by reading the PE version resource and comparing the program’s real name rather than trusting its current path.
The stack check addresses the other obvious route. Even if the registry write originates in SystemSettings.exe, UCPD can walk the stack, identify the loaded modules behind the call, and reject the operation when a blocked publisher appears in the chain.
At this point, the trust decision looked more like this:
registry operation
|
+-- protected path/value/operation?
|
+-- Microsoft-signed caller?
|
+-- denied utility by path or OriginalFilename?
|
+-- blocked process/publisher classification?
|
+-- suspicious module publisher in stack?
|
`-- allow or deny, then emit telemetry
Version 4.4: CrossDeviceResume
Version 4.4 introduced a previously undocumented rule family named CrossDeviceResume. It protects five path/value pairs:
| Path suffix | Value |
|---|---|
CrossDeviceResume\ResumeIntent |
AppIconPath |
| same | AppDisplayName |
| same | IsAppInstalled |
CrossDeviceResume\Notification |
Status |
CrossDeviceResume\Configuration |
OverlayIconPath |
In the later driver, this family is controlled by FeatureV2 bit 0x08000000 and has a dedicated CrossDeviceResume ETW label.
The names strongly suggest protection against spoofing a cross-device resume notification: which app it claims to represent, its icon, whether it is installed, and its notification state. I have not found public documentation for this internal registry schema, so that interpretation is an inference rather than a confirmed product contract.
Version 4.4 also adds regini.exe to the hardcoded deny list and makes the dynamic loader’s CertificateVerify stage explicit in its event vocabulary.
Version 4.5: GamingConfiguration
Version 4.5 added another complete policy family:
Software\Microsoft\Windows\CurrentVersion\GamingConfiguration
DefaultGamingApp
DefaultToGamingExperience
This is controlled by FeatureV2 bit 0x40000000. Unlike a coincidental registry string, it has dedicated paths for every important operation:
SetGameConfig
DelGameConfig
RenameGameConfig
DeleteGameConfigValue
GameConfig
Microsoft’s current Gaming Configuration documentation describes an OEM-configurable full-screen gaming experience, including the gaming-home app launched at startup. The public setting names differ, but DefaultGamingApp and DefaultToGamingExperience line up closely with that design.
The security motivation is sensible: a third-party program should not silently install itself as a shell-like gaming launcher or force the device to start in that experience.
Version 4.6: FeatureV3 and the broken cipher
Version 4.6 adds three particularly revealing strings:
FeatureV3
SHA512
Unknown publisher
FeatureV2 is a 32-bit REG_DWORD, and by this point Microsoft had assigned nearly every bit. FeatureV3 is a separate 64-bit REG_QWORD. The 4.7 manager’s public PDB identifies its writer exactly:
SetFeatureV3Status(__int64 mask)
It currently maps seven Windows feature IDs into bits 0 through 6. The feature traits have only numeric names, so the PDB tells us their IDs but not their product descriptions. The driver consumes those bits in its rule database and in several distinct enforcement paths.
This also dates the dynamic-rule crypto bug more precisely.
The captured DR policy was encrypted using the old MD5-derived CS64 algorithm. Version 4.6’s consumer uses SHA-512 to derive its state. The policy consequently decrypts to garbage. The loader existed in 4.2, while the incompatible SHA-512 change appears in 4.6.
That does not prove the entire mechanism can never work. It proves that the captured producer and this consumer disagree. Microsoft could correct the policy producer without changing UCPD again.
Version 4.7: the dynamic deny list becomes the baseline
The DR sample contained 14 denied Microsoft utilities:
dllhost.exe reg.exe rundll32.exe
powershell.exe regedit.exe wscript.exe
cscript.exe cmd.exe InfDefaultInstall.exe
pwsh.exe wmiprvse.exe regini.exe
bssafe.exe mshta.exe
In 4.6, the last two existed only in the captured dynamic policy. Version 4.7 adds bssafe.exe and mshta.exe to the driver’s hardcoded strings, so the static and dynamic lists finally become identical.
Version 4.7 also adds generic matching vocabulary:
*\UserChoice
*\ProgId
*\UserChoiceLatest
\ProgId
The explicit paths are still present, but these patterns suggest Microsoft was consolidating parts of the association matching logic rather than continuing to special-case every suffix.
What changed, conceptually?
Looking across all versions, UCPD evolved through four different jobs:
- Association filter — protect HTTP and HTTPS UserChoice from simple registry writes.
- Shell-setting filter — protect PDF, DeviceRegion, Feeds, taskbar, Search, Copilot, and Office associations.
- Process integrity system — track processes, publishers, loaded modules, injection rights, UI Automation, original filenames, and call stacks.
- Updatable policy engine — accept Microsoft-signed rule blobs and protect its own configuration.
The name stayed the same while the product changed underneath it.
The pattern of additions also looks much less arbitrary when placed on a timeline. Many changes are direct answers to a bypass:
| Bypass or concern | UCPD response |
|---|---|
| Use a Microsoft utility to write the key | Utility deny list |
| Use another utility not yet listed | Expanded and dynamic deny lists |
| Copy a signed utility under another name | OriginalFilename verification |
| Inject into Edge or Explorer | Process/thread handle-right filtering |
| Automate the Settings UI | Desktop/UIA restrictions |
| Inject a module into SystemSettings | Stack/module publisher inspection |
| Delete instead of setting a protected value | Delete-value enforcement |
| Reset the key ACL | SetSecurityKey enforcement |
| Rename a protected key | Rename enforcement |
| Disable features through UCPD’s own registry | Self-protection |
| Require policy changes without a reboot | Signed dynamic-rule PE |
It is an arms race preserved in version history.
How I built the corpus
I queried Winbindex for every indexed x64 UCPD.sys, then downloaded the available PE reconstruction from Microsoft’s symbol server. For each entry I recorded:
- indexed original SHA-256
- downloaded-file SHA-256
- file version and size
- Windows branch, KB, build, and release metadata
- PE sections and imports
- exception-directory function count
- ASCII and UTF-16 strings
- CodeView/PDB identity
- additions and removals relative to the adjacent version
Microsoft no longer serves the corresponding UCPD PDBs for most of this corpus, so this comparison did not depend on symbols. I grouped equivalent branch builds, ordered the remaining versions, and compared their strings, imports, and PE metadata. For important additions, I followed the string references into the machine code to confirm that they created real rules rather than being unused text.
There is an important provenance detail here. The Microsoft symbol server sometimes reconstructs the executable without its original Authenticode certificate overlay. Consequently, the downloaded file can differ from the original indexed SHA-256 and may report NotSigned even though it came from Microsoft’s symbol infrastructure.
I kept the two hashes separate and did not treat every reconstruction as independently signature-valid. Several original binaries recovered from my older .bndb files still retain their exact signatures, as does the current locally installed pair.
I also gave Codex access to the Binary Ninja databases from my earlier UCPD research, covering snapshots from March 2024 through June 2026. Symbols for UCPD.sys were missing for most versions, but Microsoft still served matching PDBs for the corresponding UCPDMgr.exe files. Those manager symbols exposed feature traits and helper names that offered another view into the changes, while the driver comparison itself remained symbol-free.
Conclusion
Following one binary over time tells a different story from analyzing one version deeply.
UCPD began as a small, narrowly focused registry filter at least as early as August 2023. Each generation responded to another way software could influence a supposedly trusted registry operation. By 2026 it had become a region- and feature-dependent kernel policy engine that watches registry operations, process creation, loaded images, object handles, UI Automation, publishers, original filenames, and stacks—and can consume signed policy from the registry.
The most interesting additions are not even about the default browser anymore. Cross-device resume prompts and the default gaming experience are now part of the same protection framework.
I suspect FeatureV3 is a good indication of what happens next. FeatureV2 ran out of room, but Microsoft is clearly not finished adding rules.
References
- The Binary Hiding in Your Registry: Cracking Windows UCPD’s Dynamic Rules
- Inside Windows’ Default Browser Protection
- UCPD.sys – UserChoice Protection Driver Part 2
- UserChoice Protection Driver – UCPD.sys
- UserChoiceLatest – Microsoft’s new protection for file type associations
- KB5121767 Enables Additional UCPD.sys Features on Windows 11 Enterprise
- Windows UserChoice Protection Driver UCPD
- UCPD analysis databases and tooling
- Winbindex