Binary Ninja Blog

Inside Windows' Default Browser Protection

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.

Default browser hash

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:

  1. The executable is signed by Microsoft
  2. The executable is NOT in a list of utility programs, including:
    • reg.exe
    • rundll32.exe
    • powershell.exe
    • regedit.exe
    • wscript.exe
    • cscript.exe
    • …etc

UCPD logic

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:

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!

Interesting strings

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 or msedge.exe
  • Type 0x8:
    • The process is kwsprotect64.exe, kxescore.exe, or 360Tray.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.
  • Type 0x10: the executable is signed by Opera 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.

Interesting strings

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:

Registering object callbacks

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 or msedge.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 rights 0x28 (PROCESS_VM_OPERATION | PROCESS_VM_WRITE) are removed
  • For PsThreadType, the thread access right 0x10 (THREAD_SET_CONTEXT) is removed

Object callbacks limit requested access rights

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):

Desktop object callback removing accessi rights

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:

Registering object callbacks

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:

SetUCPDStatus in UCPDMgr.exe

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:

SetUCPDStatus in UCPDMgr.exe

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:

  • 0x200UCPD_NEWDENYLIST, which enables a new deny list for registry keys
  • 0x800UCPD_RENAME_ATTACK, which protects against an attack that renames certain keys
  • 0x8UCPD_WSB, which protects the Windows search bar
  • 0x10UCPD_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:

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.

UCPD examines the stack trace

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.

UCPD collects processes and loaded modules

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.

References