Change theme
Help
Press space for more information.
Show links for this issue (Shortcut: i, l)
Copy issue ID
Previous Issue (Shortcut: k)
Next Issue (Shortcut: j)
Sign in to use full features.
Vote: I am impacted
Notification menu
Refresh (Shortcut: Shift+r)
Go home (Shortcut: u)
Pending code changes (auto-populated)
Common Vulnerability Exposure (CVE) identifier [ID: 1352857]
[ID: 1352883]
Who found the vulnerability? [ID: 1352754]
The methodology which was used to find the vulnerability [ID: 1352909]
Product (subset of Vendor) [ID: 1352755]
Vendor (superset of Product) [ID: 1352808]
ID provided to the Project Zero team by the vendors [ID: 1352833]
View issue level access limits(Press Alt + Right arrow for more information)
Attachment actions
Unintended behavior
View staffing
Description
In Windows Registry, there is a special type of keys called predefined-handle keys (let's call them predefined keys for short). It's a form of a "symbolic link" of sorts, where the key doesn't fully exist on its own, but is instead a placeholder that translates to a predefined 32-bit handle (e.g. HKEY_PERFORMANCE_TEXT, etc.) whenever referenced through WinAPI. Since predefined keys have the special function, most registry-related system calls don't allow operations on them. To facilitate the check, there is an internal kernel routine CmObReferenceObjectByHandle, which is a special wrapper around ObReferenceObjectByHandle used to reference key handles provided by user-mode clients. It has the additional logic of returning STATUS_INVALID_HANDLE whenever a predefined key is referenced, which allows syscall handlers like NtSetValueKey or NtDeleteKey to handle these cases safely.
The bug described in this report is in the Registry Virtualization feature [1] present in the OS since Windows Vista. It is responsible for redirecting accesses that reference global, admin-writable keys (such as most of HKEY_LOCAL_MACHINE) and transparently pointing them to user-accessible locations, so that legacy applications that expect to always run with administrative privileges can continue to work under more restricted user accounts. For example, with registry virtualization enabled, writes to HKLM\Software are silently translated to HKCU\Software\Classes\VirtualStore\Machine\Software by the kernel. The three high-level functions responsible for remapping/replicating "real" keys to "virtual" ones are CmKeyBodyRemapToVirtual, CmKeyBodyRemapToVirtualForEnum and CmKeyBodyReplicateToVirtual. The more low-level functions performing the string-based translations between the two key types are CmRealKCBToVirtualPath and CmVirtualKCBToRealPath.
The problem is that while references to keys via handles are subject to checks against predefined keys (which are then rejected), the same logic isn't applied to registry paths accessed through registry virtualization. In the above example, if HKCU\Software\Classes\VirtualStore\Machine\Software was an existing predefined key, an internal registry function would open it and start operating on it, even though it assumes that the key in question is not a predefined one.
According to our knowledge, new predefined keys cannot be currently created using any standard system APIs. However, there are still three different ways how they can exist in registry:
For exploitation purposes, option #1 doesn't work because the default predefined keys are not really predefined in the full sense of the word. Since they are created at run time, they have the 0x40 flag set in _CM_KEY_CONTROL_BLOCK.Flags, but they don't have the _CM_KEY_NODE.ValueList structure set to the same values as a hive loaded from disk would (which is a prerequisite for certain exploitation scenarios). Furthermore, since they reside in HKLM, they can only be reached through the virtual store -> real store translation, and not the other way around.
Furthermore, option #2 doesn't seem to work either, because app hives can only be loaded under the \Registry\A namespace, and at the same time, the kernel doesn't allow keys under \Registry\A to be opened using the full path (only references to keys relative to an already opened app hive handle are permitted). In registry virtualization, keys are always opened using full rewritten paths, which makes this option infeasible.
This leaves us with option #3. In the rest of the report and in the proof-of-concept exploits, we will assume that there is a HKCU\PredefinedKey predefined key. In our test environment, we configured it to have the 0x100 (VirtualTarget) and 0x40 (PredefinedHandle) flags set in _CM_KEY_NODE.Flags, _CM_KEY_NODE.ValueList.Count set to 0x80000000, and _CM_KEY_NODE.ValueList.List set to 0xCCCCCCCC. You can reproduce the same setting by creating a regular HKCU\PredefinedKey key in Regedit, and then pulling the NTUSER.DAT file off of the test VM, using the attached ConvertNtuserDat.cpp program to binary patch the hive file, and copying it back as NTUSER.MAN to the user home directory (or following the two-user attack scheme). In a real scenario, a local attacker could save a specially crafted NTUSER.MAN file to disk that wouldn't need to be based on the existing NTUSER.DAT, but would still allow the user to log in and perform the attack.
Operating on predefined keys through registry virtualization is a general class of problem; we have found two specific examples of how it can be exploited in practice. Both of them are discussed below.
========== Operating on key values ==========
The biggest risk associated with having registry syscalls operate on predefined keys is related to key values. This is due to the dual use of the _CM_KEY_NODE.ValueList structure: for a normal key, it has the
Count
field set to the number of values, andList
set to the cell index of a list of indexes pointing to _CM_KEY_VALUE structures; but for a predefined key,Count
is used to store the 32-bit value of the predefined handle (with the highest bit always set), andList
is ignored and can have any value. So in case of a type mismatch, we get a "type confusion", where the code thinks the key has a huge number of values pointed to by an arbitrary cell index.Such a condition can lead to a number of unsafe primitives, depending on:
We think that the bug can be exploited for both information disclosure and privilege escalation through memory corruption. In the current implementation, by having full control over the 32-bit cell index (_CM_KEY_NODE.ValueList.List), it is possible to have the kernel translate it to an arbitrary virtual address and operate on it for both reads and writes.
In the proof-of-concept exploit attached to this report, we've decided to exploit the bug through value enumeration, as it's relatively easy to achieve and demonstrates the bug well. The steps taken by the program are outlined below:
An example crash log, generated on Windows 11 (September 2022 update), is shown below:
--- cut ---
*******************************************************************************
* *
* Bugcheck Analysis *
* *
*******************************************************************************
SYSTEM_SERVICE_EXCEPTION (3b)
An exception happened while executing a system service routine.
Arguments:
Arg1: 00000000c0000005, Exception code that caused the bugcheck
Arg2: fffff8063f0af37c, Address of the instruction which caused the bugcheck
Arg3: ffffcb85a9fc5cb0, Address of the context record for the exception that caused the bugcheck
Arg4: 0000000000000000, zero.
[...]
CONTEXT: ffffcb85a9fc5cb0 -- (.cxr 0xffffcb85a9fc5cb0)
rax=0000000000000000 rbx=0000000000001320 rcx=ffff9a0fc309c000
rdx=0000000000001320 rsi=ffffcb85a9fc6790 rdi=0000000000000ccc
rip=fffff8063f0af37c rsp=ffffcb85a9fc66d8 rbp=ffffcb85a9fc67f9
r8=ffffcb85a9fc6794 r9=00000000000000cc r10=ffff9a0fc309c000
r11=ffffcb85a9fc6740 r12=ffff9a0fc509be60 r13=0000000000000000
r14=00000179a5652610 r15=ffff9a0fc41aac50
iopl=0 nv up ei ng nz na pe nc
cs=0010 ss=0018 ds=002b es=002b fs=0053 gs=002b efl=00050282
nt!HvpMapEntryGetBinAddress:
fffff806
3f0af37c 488b4208 mov rax,qword ptr [rdx+8] ds:002b:00000000
00001328=????????????????Resetting default scope
PROCESS_NAME: Registry
STACK_TEXT:
ffffcb85
a9fc66d8 fffff806
3ed4d12a : ffffcb85a9fc67f9 ffffcb85
00000001 0000000000000003 00000000
00000000 : nt!HvpMapEntryGetBinAddressffffcb85
a9fc66e0 fffff806
3ed4d0f2 : ffffcb85a9fc66c0 00000000
00000000 000f003f00020039 00000000
00000000 : nt!HvpMapEntryGetBlockAddress+0xeffffcb85
a9fc6710 fffff806
3ef115ae : 0000000000000001 00000000
00000000 ffff9a0fc509be60 fffff806
3e8c2e3c : nt!HvpGetCellPaged+0x72ffffcb85
a9fc6740 fffff806
3eeba53a : 0000000000000000 00000000
00000000 0000000000000000 000001e5
00000000 : nt!CmEnumerateValueKeyFromMergedView+0x212ffffcb85
a9fc6840 fffff806
3ea2d375 : 0000000000000000 00000000
00000000 0000000000000000 00000000
00000000 : nt!NtEnumerateValueKey+0x15817affffcb85
a9fc6a70 00007ffc
90063de4 : 0000000000000000 00000000
00000000 0000000000000000 00000000
00000000 : nt!KiSystemServiceCopyEnd+0x2500000071
908ff6d8 00000000
00000000 : 0000000000000000 00000000
00000000 0000000000000000 00000000
00000000 : 0x00007ffc`90063de4--- cut ---
========== Deleting a predefined key ==========
Deleting a predefined key is not possible under normal circumstances, because the NtDeleteKey system call uses the CmObReferenceObjectByHandle function to detect such keys early and return with a STATUS_INVALID_HANDLE status code. Nevertheless, it is possible to point the kernel at a predefined key at a later stage of the syscall execution, through registry virtualization. Suppose we create the following key (which is possible since the DRM subkey is world-writable):
HKLM\Software\Microsoft\DRM\TestKey
If we set its security such that it's world-readable but only admin-writable, set the 0x80 flag (VirtualSource) on this key, and enable virtualization for our process, then the key will be subject to virtualization via the CmKeyBodyRemapToVirtual function. The kernel will then translate it to a "virtual", user-accessible path:
HKCU\Software\Classes\VirtualStore\Machine\Software\Microsoft\DRM\TestKey
When this path is opened, symbolic links are followed, so we can use them to point this key to our predefined one at:
HKCU\PredefinedKey
In this way, an attacker can have the internal CmDeleteKey function called against a predefined key, which should normally never be possible. But how can this behavior be exploited?
To understand it, we have to look even deeper and into the CmpFreeKeyByCell function (called by CmDeleteKey). It is responsible for unlinking a key from its parent, and freeing all the cells related to the key. The freeing part has some strange logic for handling certain special types of keys, such as predefined keys. Let's have a look at the following pseudo-code, stripped down to just operations related to security descriptors:
--- cut ---
1 VOID CmpFreeKeyByCell(_HHIVE *Hive, DWORD Cell) {
2 _CM_KEY_NODE *KeyNode = Hive->GetCellRoutine(Hive, Cell);
3
4 if ((KeyNode->Flags & (PREDEFINED_HANDLE | EXIT_NODE)) == 0) {
5 if (KeyNode->Security != -1) {
6 CmpFreeSecurityDescriptor(Hive, Cell);
7 }
8 }
9
10 CmpFreeKeyBody(Hive, Cell);
11 }
12
13 VOID CmpFreeSecurityDescriptor(_HHIVE *Hive, DWORD Cell) {
14 _CM_KEY_NODE *KeyNode = Hive->GetCellRoutine(Hive, Cell);
15
16 if (KeyNode->Security != -1) {
17 _CM_KEY_SECURITY *Security = Hive->GetCellRoutine(Hive, KeyNode->Security);
18
19 if (Security->ReferenceCount == 1) {
20 CmpRemoveSecurityCellList(Hive, KeyNode->Security);
21 HvFreeCell(Hive, KeyNode->Security);
22 } else {
23 Security->ReferenceCount--;
24 }
25
26 KeyNode->Security = -1;
27 }
28 }
29
30 VOID CmpFreeKeyBody(_HHIVE *Hive, DWORD Cell) {
31 _CM_KEY_NODE *KeyNode = Hive->GetCellRoutine(Hive, Cell);
32
33 if ((KeyNode->Flags & EXIT_NODE) == 0) {
34 if (KeyNode->Security != -1) {
35 HvFreeCell(Hive, KeyNode->Security);
36 }
37 }
38
39 HvFreeCell(Hive, Cell);
40 }
--- cut ---
As we can see, there are two places where the freeing of an SD can happen: one is the entire CmpFreeSecurityDescriptor function as called by CmpFreeKeyByCell, and the other in CmpFreeKeyBody, in lines 34-36. Security descriptors in registry hives are reference-counted, so CmpFreeSecurityDescriptor correctly decrements the counter and only frees the cell once it reaches 0. On the other hand, the code in lines 34-36 frees the cell unconditionally, without taking refcounts into account at all.
We are not sure why there is this overlap in functionality between CmpFreeKeyByCell/CmpFreeSecurityDescriptor and CmpFreeKeyBody, especially that the behavior of the latter seems obviously incorrect and can lead to use-after-free conditions as a result of freeing SDs that still have multiple active references. So why does this code not corrupt hives on a regular basis?
That's because in the normal case, keys being deleted don't have the Predefined/Exit flags set, so the code in lines 5-7 kicks in, CmpFreeSecurityDescriptor correctly drops a reference to the SD, and resets the _CM_KEY_NODE.Security field to -1 in line 26. When CmpFreeKeyBody is called afterwards, the if condition in line 34 evaluates to false, and the free in line 35 is never executed. However for predefined handles, CmpFreeKeyByCell proceeds directly to CmpFreeKeyBody, which frees the SD regardless of how many other keys still use it. As mentioned earlier this may lead to a UAF of the security descriptor, which can be turned into a UAF of any type of cell, and subsequently lead to a privilege escalation in the system.
The issue can be reproduced with the attached RegDeleteKey proof-of-concept exploit. It performs the following steps:
Due to the fact that the UAF applies to a cell inside the User Hive, which may have a different structure/layout on different systems depending on multiple factors, it is difficult to create a reproducer that reliably crashes in exactly the same way in all environments. For this reason, our PoC stops at freeing the security descriptor cell, but doesn't take any extra steps to fill the freed space with new data or have it used to force a crash.
Nevertheless, we can observe the buggy behavior by attaching WinDbg to a test VM, setting a breakpoint on the HvFreeCell call inside CmpFreeKeyBody that frees the security descriptor, and investigating the SD once the proof-of-concept exploit is run:
--- cut ---
0: kd> g
Breakpoint 0 hit
nt!CmpFreeKeyBody+0x15be73:
fffff800
2b4b903f e8a026eaff call nt!HvFreeCell (fffff800
2b35b6e4)0: kd> !reg cellindex @rcx @rdx
Map = ffff8004a5463128 Type = 0 Table = 0 Block = 27 Offset = 188
MapTable = ffff8004a2175000
MapEntry = ffff8004a21753a8
BinAddress = 00000231e7b68001, BlockOffset = 0000000000000000
BlockAddress = 00000231e7b68000
pcell: 00000231e7b6818c
0: kd> dt _CM_KEY_SECURITY 00000231e7b6818c
nt!_CM_KEY_SECURITY
+0x000 Signature : 0x6b73
+0x002 Reserved : 0
+0x004 Flink : 0xd5b70
+0x008 Blink : 0xd7200
+0x00c ReferenceCount : 0x232
+0x010 DescriptorLength : 0xb8
+0x014 Descriptor : _SECURITY_DESCRIPTOR_RELATIVE
--- cut ---
Here we first translate the cell index being freed to a virtual address using the WinDbg !reg extension, and then dump the corresponding _CM_KEY_SECURITY structure. It is apparent that the security cell is being freed even though ReferenceCount is set to 0x232, which leaves 0x231 other keys in the hive with invalid references to the security descriptor.
We strongly recommend a closer investigation/refactoring of the logic of CmpFreeKeyBody and its interactions with CmpFreeKeyByCell/CmpFreeSecurityDescriptor, to ensure that there aren't any more variants of the UAF in the future, if there is another code path allowing the deletion of predefined keys.
This bug is subject to a 90-day disclosure deadline. If a fix for this issue is made available to users before the end of the 90-day deadline, this bug report will become public 30 days after the fix was made available. Otherwise, this bug report will become public at the deadline. The scheduled deadline is 2023-01-04.
References:https://learn.microsoft.com/en-us/windows/win32/sysinfo/registry-virtualization https://learn.microsoft.com/en-us/windows/client-management/mandatory-user-profile
[1]
[2]