The battle for the default browser on Windows has always been heated. You might have heard of how Microsoft leveraged its UCPD (User Choice Protection Driver) to prevent third-party browsers from setting themselves as the default one. However, in this post, I will show my journey into uncovering how various browsers try to bypass the restriction, and how UCPD gets updated to defeat their attempts.
Note: this is an extended version of my lightning talk at RE//verse. Please also check out the video and slides.
Background
The default browser is saved in the following registry keys:
HKEY_CURRENT_USER\Software\Microsoft\Windows\Shell\Associations\UrlAssociations\http\UserChoice
HKEY_CURRENT_USER\Software\Microsoft\Windows\Shell\Associations\UrlAssociations\https\UserChoice
Once upon a time, setting the default browser was as easy as setting the value of these keys – which is relatively straightforward. However, this was often abused by certain vendors to hijack the default browser to their own, without user consent or even interactions.
To address this issue, Windows introduced a Hash
sub-key which contains a hash value of the selected default browser.
The default browser settings are only respected if the hash is correct. The hash algorithm is proprietary, and
can only be calculated correctly if you use the Windows’s Settings dialog to set it. It also includes entropy from the
current machine so it cannot be pre-computed.
And of course, the secret for this hash would not last very long. In 2017, Christoph Kolbicz reverse-engineered the hash algorithm and deployed it in his SetUserFTA tool, a command line utility that can set file type associations or default browser. In 2021, Mozilla similarly enabled Firefox to set itself as the default browser directly.
Microsoft responded to the “cracking” of its hash algorithm with the introduction of the UCPD driver in March 2024. UCPD stands for User Choice Protection Driver, and it is a filter driver that protects the registry keys that store the default browser settings (along with similar things, e.g., the default PDF reader).
Gunnar Haslinger first reported the discovery of the sneaky UCPD driver and analyzed its functionality. In short, it uses the standard Window registry filtering mechanism – CmRegisterCallbackEx – to register a callback that monitors the registry operations. If a protected key is being operated on (e.g., edited, created, etc), it only allows it if the requesting process is trusted. The criteria for a trusted process are:
- The executable is signed by Microsoft
- The executable is NOT in a list of utility programs, including:
reg.exe
rundll32.exe
powershell.exe
regedit.exe
wscript.exe
cscript.exe
- …etc
Interestingly, I was lucky enough to catch a glimpse of the driver before Microsoft removed its symbol file from the PDB server. Based on the function names and the implementation, it is easy to see its intention to stop any third-party from modifying the default browser registry keys and enforcing that the only way to set the default browser is through the Settings app:
If you think about this carefully, you will realize that UCPD still allows the Edge browser to set itself as the default browser (it is signed by Microsoft and it is not in the utility list). However, per my tests, Edge is NOT utilizing this to force itself to be the default browser – when you instruct Edge to set itself as the default browser, it launches the Windows Settings app in which you will need to select it as the default by yourself – which is the officially recommended way.
Are you already surprised that Microsoft even bothers to create a driver to protect the default browser? Well, bear with me since this is only the start of the story!
Injection is Not Allowed
As a hobbyist security researcher, I am curious to see if there is a way to bypass the UCPD. I did not have the time to dig into it until late October. At that time, my Windows PC was already updated to the build 24H2. And I was quickly welcomed by a surprise – the new UCPD driver contained the names of several well-known vendors!
What is going on?
To begin with, I noticed that the new driver uses PsSetCreateProcessNotifyRoutineEx to monitor all the new process creation. For each created process, it first checks if its image file name contains any of the following sub-strings:
\safemon\
\kingsoft\
\opera\
\msedge.exe
\explorer.exe
If so, for each such process, it classifies the processes into several different categories and assigns an enum value to it. The categories are:
- Type 0x4: the process is
explorer.exe
ormsedge.exe
- Type 0x8:
- The process is
kwsprotect64.exe
,kxescore.exe
, or360Tray.exe
, and - The executable is signed by one of the following:
Beijing Kingsoft Security software Co.,Ltd
Beijing Qihu Technology Co., Ltd.
Zhuhai Juntian Electronic Technology Co., Ltd.
- The process is
- Type
0x10
: the executable is signed byOpera Norway AS
, likely the Opera browser
The process -> category mapping information is saved into an
AVL table.
If you are not familiar with it, you can think of it as a std::map
equivalent. You can find the main processing logic
of the process notify routine in the below screenshot. Within it, the element.type
is the process category mentioned
above.
And where is the saved mapping information used? Well, it is used in the object callbacks registered with ObRegisterCallbacks.
There are three object callbacks, one for each of PsProcessType
, PsThreadType
, and ExDesktopObjectType
.
The callbacks are all PreOperation
so that they can examine the requests and act accordingly:
The two PreOperation callbacks for the PsProcessType
and PsThreadType
are the same. Within it, it first gets the
requesting process that is trying to acquire a handle, and the target process (or the process the target thread belongs
to) whose handle is being acquired. It then uses the PID to look up the process category information from the AVL table
it maintains. After that, it checks for the following conditions:
- The target process has a category type
0x4
(explorer.exe
ormsedge.exe
) - The requesting process has a category type
0x8
(360Tray.exe/kxescore.exe/kwsprotect64.exe
)
If the condition is met, then it removes the following access rights from the requested rights:
- For
PsProcessType
, the process access rights0x28
(PROCESS_VM_OPERATION
|PROCESS_VM_WRITE
) are removed - For
PsThreadType
, the thread access right0x10
(THREAD_SET_CONTEXT
) is removed
Now it should be quite easy to see the intention – it is preventing 360Tray.exe/kxescore.exe/kwsprotect64.exe from injecting code into the explorer.exe/msedge.exe! Why would UCPD bother doing that? The only explanation is they are trying to bypass UCPD by injecting code into explorer.exe/msedge.exe since the two can modify the registry key for the default browser. And Microsoft did not like the idea, so it tightened its protection by directly banning the offenders!
The remaining callback for ExDesktopObjectType
checks if the current process has a category 0x10, i.e., the Opera
browser. The code is simple – it just removes the access right 0x20 (by &= 0xffffffdf
):
But it took me some time to figure out what it meant. To start with, we can see 0x20 corresponds to
DESKTOP_JOURNALPLAYBACK
in the official docs on
“Desktop Security and Access Rights”.
The docs say the access right is “required to perform journal playback on a desktop”, which I had no idea about. I found
little information about it after Googling, so I asked
ChatGPT got some clue – it is part of the Windows
UI Automation and meant to be used to
playback previously recorded user keyboard and mouse inputs. ChatGPT even helpful pointed out the security
considerations related to it – that it can be abused for bogus UI interactions.
Chances are the Opera browser is playing some UI tricks to set the default browser, e.g., by interacting with the Settings app directly. UCPD has no mercy for it.
UCPD Manager
You might be wondering why I am so sure about my conclusion. As I dig deeper into this, I found the handle protection is only enabled when a global flag has the bit 0x100 set – otherwise, it will do nothing:
This flag value is read from the following registry key:
HKEY_LOCAL_MACHINE\SYSTEM\ControlSet001\Srevices\UCPD\FeatureV2
Meanwhile, there is a UCPDMgr.exe
(UCPD Manager) in the system directory, whose SetUCPDStatus
can set the value of
this registry key:
Apparently the bit 0x100
is set when
wil::Feature<struct __WilFeatureTraits_Feature_UCPD_ANTIINJECTION>::GetImpl::impl::~impl
is enabled. And the
ANTIINJECTION
(anti-injection) in the name is a solid confirmation!
Another value that caught my eye is 0x80
, which goes by the name UCPC_ANTIUIA
. I figured the ANTIUIA
means
anti-UI attack
, and it reminds me of the desktop object callback above. I checked the start of the desktop callback
function and all dots are connected:
The check if ((enabled_feature).b s< 0
is indeed checking whether the bit 0x80 is set – since it takes the low byte
from enabled_feature
and checks if it is negative, which is equivalent to checking if the highest bit is set.
There are also several other interesting strings in it:
0x200
–UCPD_NEWDENYLIST
, which enables a new deny list for registry keys0x800
–UCPD_RENAME_ATTACK
, which protects against an attack that renames certain keys0x8
–UCPD_WSB
, which protects the Windows search bar0x10
–UCPD_TASKBAR
, which protects the taskbar
For the sake of time, I did not dig into each of these. If you are interested, please feel free to check it out by yourself!
Another interesting bit is that when the handle protection takes action to limit the access rights, an event is
generated in event tracing. From the content of the event, we can see that the UCPD driver I looked at is version
3.1.0.0
. Coincidentally, the earliest UCPC driver I have, i.e., the one still has the symbol, is version 2.1.0.1
.
So I must have missed the very first version of it!
Stacktrace Must be Checked
While preparing this blog, the Windows March 2025 update dropped and it comes with UCPD 4.0. This time, some 16 vendors are added to a new block list:
It turns out that UCPD is getting tougher again. To start with, the list is used when the bit 0x400
is set in the
feature DWORD. And in the new UCPDMgr.exe, it is associated with UCPD_BACKTRACE
. And yes, UCPD is now examining the
stack trace to determine if a registry operation should be allowed!
In short, when its registry
callback sees SetValueKey/DeleteKey/RenameKey is called on a protected key, in addition to the existing
IsTrustedProcess
check, now checks whether the requesting process is SystemSettings.exe
(the official app to
set the default browser). And if so, it examines the stack trace with RtlWalkFrameChain
. If any of the frames contains
a module signed by a blocked vendor, then the operation is rejected.
To be able to do so in an efficient way, UCPD now leverages both PsSetCreateProcessNotifyRoutineEx
and
PsSetLoadImageNotifyRoutine
to record the list of active processes and their loaded modules. The results are stored
in an AVL table, along with some metadata.
Conclusion
In this post, I tracked how the UCPD driver is evolving across different versions. Looking back, it is quite surprising to me that both sides went so far in the fight for the default browser, and I believe the trend will go on. I hope you enjoyed reading it! If you wish to check things out by yourself, you can find my analysis database here.