Exploring theWindows Registry
as a powerful LPE
attack surface
Mateusz Jurczyk
Microsoft BlueHat, October 2023

Registry: Lines of kernel code (decompiled)Timeline of most important features
Initial implementation
Predefined Keys
Early hive versions: 1.1, 1.2
Major version 1.3 (Fast Leaf support)

KTM Transactions
Registry Virtualization
App Keys

Lightweight Transactions
Containers: Namespace Redirection
Containers: Differencing Hives
Major version 1.6 (Layered Key support)

Key Rename
Registry Callbacks
Minor version 1.4 (Big Value support)
Major version 1.5 (Hash Leaf support)

Windows NT 3.1 - 4.0(1993 – 1996)

Windows XP(2001)
Windows 2000
(2000)

Windows Vista(2007)
Windows Server 2003
(2003)

Windows 10 1607
(2016)

Windows 7, 8, 8.1(2009 – 2013)
Windows 10 1507, 1511
(2015)

Windows 10/11 1703 – 22H2
(2017 – 2022)

Why attack the registry?
• Local attack surface potentially allowing privilege escalation in the system• Stores and operates on sensitive data (system configuration, user credentials)• Many potential types of issues:– Plain memory corruption– Logic bugs
– Information disclosure– Inter-process disruption (registry as a shared resource)• Huge old/new C codebase with layers of complex mechanisms mixed together

Prior publicly known security research• Evidently a lot of work done internally at Microsoft• Relatively little prior art in the public space– 2010: 5 bugs reported by Gynvael Coldwind and myself– 2014 – 2020: 17 bugs by James Forshaw
 A consistent stream of kernel logic issues, many at theintersection of registry and other system mechanisms(security impersonation, file system)– 2016: 4 bugs reported by James and me as a result ofsome basic hive format fuzzing– 201X: Several isolated bugs reported by others (Fortinet,
Maxim Suhanov)This effort• Started in May 2022 as a test of my new coverage-based fuzzer• Found one (1) and only bug: GPZ-2299/CVE-2022-35768– Windows Kernel multiple memory problems when handling incorrectly
formatted security descriptors in registry hives– Checks out as security descriptors are one of the few things well-suitedfor binary fuzzing• The initial success prompted me to have a deeper look into

the kernel• It quickly turned into a challenge to review all of the code...

The research process

04

Reverse engineer

Test, reproduce, reportbugs

Choose a self-contained part of the registry
implementation and try to get it as close toreadable C-like code as possible.

Test any discovered bugs, create reliablereproducers, write up detailed reports andsubmit them to Microsoft.

03

01

Compare with priorknowledge

Understand the logic
Try to understand the purpose, assumptions,guarantees and underlying intentions of
the code.

02

Consider if the behaviour of the feature isconsistent with what we already know about
the registry.

Research progression: Major features

Hive loading

Basic operations

Registry virtualization

Transactions

Differencing hives

Registry callbacks

Results as of September 2023

Really hard to quantify
actual number of bugs
(what is a "bug"?)

Official classification:• 33 issues marked as Fixed in the Project Zero

• 33 x Windows Kernel Elevation of Privilege• ~45 unique problems (in my assessment)• 5 x Windows Kernel Information Disclosure

bug tracker• 39 CVEs assigned by Microsoft• ?? fixes introduced in the source code

Vulnerability
Vulnerability• 1 x Windows Kernel Memory Information

Disclosure Vulnerability

Reverse engineering

Reverse engineering the Windows Kernel• An essential step: It probably took around 70-80% of the total

research time

• Analysis primarily done on Windows Server 2019 (originally used for

fuzzing), then reproduced on up-to-date Windows 11• Tools: IDA Pro disassembler with Hex-Rays decompiler• Extra aid: PDBs for ntoskrnl.exe hosted on the Microsoft Symbol Server– Function names, global variable names, some structure layouts, someenum definitions

BeforeAfter

The hard part• Dealing with compiler optimizations: inlined functions, mangled arithmetic etc.
– Sometimes necessary to cross-check the same code in different builds of Windows• Figuring out the missing pieces– Reconstructing internal structures: the parse context, on-disk transaction log records, some structures
related to virtualization/transactions, all structures related to differencing hives– Reverse-engineering the meaning and names of constants – basically guesswork
 Both static and dynamic analysis employed to try to deduct their meaning Many of them still poorly/not understood• Dear Microsoft: please publish more information through the public symbols, every bit helps

Understanding the code

Understanding the code
• Once the code is readable, we can analyze a particular feature asa whole– What problem is it trying to solve?
– Is it implemented the same way I would intuitively do it? If not, why?
– Is it internally consistent?
– Does it correctly handle error conditions?
– Does it behave in accordance with the documentation?

– Does it make any assumptions that aren't explicitly enforced?
– What interesting primitives does it enable?
(even if they're not bugs on their own)

• Exposes deeper, logic bugs and structural weaknesses

Examples of deep-rooted bug classes

Resource exhaustion

Handling partial successin multi-step operations

Constraints of the hive
binary format:
Expectations vs reality

Resource exhaustion• Every "create" and "set" operation internally (re)allocates buffers from the hive storage or kernel pools• A local attacker may try to interfere by exhausting both types of memory:– Hive storage: allocation failure very practical and easy to trigger– Kernel pools: allocation failure possible but slightly less practicalHive size limit exhaustion• Two separate quotas enforced on registry size:– The maximum size of a single hive is 4 GiB– The cumulative system-wide registry quota is also 4 GiB• That's two ways to reliably cause HvAllocateCell to fail• Opens up a plethora of interesting, deep error code paths to review– Every such path needs to restore the registry to a known-good state– Most of them are probably poorly tested, as they almost never trigger in real life

• One of my main focuses throughout the research

Resource exhaustion – Allocation call sites

Partial success of multi-step operations• Error handling gets even more difficult across function boundaries• Information about outcome is passed back via a NTSTATUS return value– In theory a 32-bit type, in practice mostly used as a binary success/failure differentiator– Semantically more of a "last error encountered" than "overall operation status"• Not many, but there are some functions implementing multi-step operations– CmpReplicateKeyToVirtual – recreates a virtualized key in the user's hive (virtual store)– CmpTransMgrCommit – commits an entire transaction, which may consist of an unlimited numberof operations

Error handling-related bugs
• GPZ-2330: Windows Kernel registry use-after-free due to bad handling of failed reallocations undermemory pressure• GPZ-2369: Windows Kernel use-after-free due to dangling registry link node under paged pool memory

pressure• GPZ-2375: Windows Kernel multiple issues in the key replication feature of registry virtualization• GPZ-2394: Windows Kernel multiple issues in the prepare/commit phase of a transactional registry

key rename• GPZ-2410: Windows Kernel CmpCleanupLightWeightPrepare registry security descriptor refcount leakleading to UAF• GPZ-2433: Windows Kernel KTM registry transactions may have non-atomic outcomes• GPZ-2456: Windows Kernel partial success of registry hive log recovery may lead to inconsistent state andmemory corruption

Constraints of the hive format• When loading a hive, the kernel performs

extensive sanity checks of its structure– CmpCheckKey, CmpCheckValueList, many other
CmpCheck* functions

Accepted by thehive loader• While quite strict, the checks still allow(ed) someconstructs that would never be produced by the
kernel itself: by mistake or by design

Written by the
kernelOdd-but-accepted construct examples
Hive data construct

Accepted by loader

Written by kernel

Large cells >16 KiB not aligned to power of two✅

❌

Non-compressed ASCII-only key names✅

❌

Empty subkey lists✅

❌

Leaf subkey lists longer than 507/1013 elements✅

❌

Subkey list types incompatible with hive version✅

❌

Unused security descriptors✅

❌

Duplicate security descriptors✅

❌

Values with duplicate names✅

❌

Odd-but-accepted construct examples
Hive data construct

Accepted by loader

Written by kernel

Large cells >16 KiB not aligned to power of two✅

❌

Non-compressed ASCII-only key names✅

❌

Empty subkey lists✅

❌

Leaf subkey lists longer than 507/1013 elements✅

❌

Subkey list types incompatible with hive version✅

❌

Unused security descriptors✅

❌

Duplicate security descriptors✅

❌

Values with duplicate names✅

❌

Interactions between features

Interactions between features• The original registry design from Windows NT seems defendable– Simple, predictable operations that work exactly as advertised• Then increasingly complex features got introduced, complicatinginternal state– KCB size: 64 bytes in Windows 2000, 176 bytes in Windows 10 22H2

• Each of them implements some kind of ✨magic✨ that may not be

obvious to other parts of the kernel• Let's consider how they deviate from the baseline mental modelSpecial types of keys

Symbolic links (flag 0x10)

Predefined-handle keys (flag 0x40)• Transparently point to another registry key, if• Transparently point to a chosen predefined key• Makes it harder to reason if opening a key really• Have no values, the ValueList part of key node is• Often useful as an exploitation primitive• Cannot be operated on by most syscalls – supposed

opened with default options

opened that key, or even the intended hive

• Can be created via API, used extensively by

Windows itself(HKLM, HKCU, HKCR, etc.)

reused for a different purpose
to only ever be opened

Registry virtualization• Compatibility mechanism to create the illusion of running as

administrator• Redirects and replicates operations within the system-wide

HKLM\Software hive to a user-accessible copy in
HKCU\Software\Classes\VirtualStore• Outcome: creation of a key in a different location than specified, or

reading from multiple sources when the caller thinks it's just one key

Transactions
Normal registry: Everything either is or isn't
Key doesn't exist

Create key

Key exists

Transacted registry: Everything is, isn't, or is pending

CommitKey doesn't exist

Create key

Key exists

Key exists intransaction

RevertKey doesn't exist

Transactions• There is no ground truth about the state of the registry, everything is

considered in the scope of the specific alternate reality (transaction)• A key may exist in the global view but not in a transaction, and viceversa

– All aspects of keys may be subject to alternate states: name, subkeys, values,
security• Significant complexity added to all registry code
– Non-transacted write operations must revert pending transactions
concerning the given key– Transacted write operations must be careful to avoid collisions with otherexisting transactions– All read operations must correctly incorporate transacted stateDifferencing hives• Windows 10 1607 added another huge complication to support containers: differencing hives• Normal hives are standalone, self-sufficient databases for storing data

• Delta hives are "patch sets" to be applied to another hive (base or delta)• They can be stacked on top of each other in case of nested containers• A key referenced through a differencing hive is a layered key

Normal registry tree

Root

Differencing hives tree

Layer height 2Layer height 1

Layer height 0 (base hive)

Layered key tree

Mergeunbacked

Tombstone

Supersedetree

Layer height 2

Supersedelocal

Mergebacked

Layer height 1

Mergebacked

Layer height 0 (base hive)

Layered key stack

Mergeunbacked

Tombstone

Supersedetree

Layer height 2

Supersedelocal

Mergebacked

Layer height 1

Mergebacked

Layer height 0 (base hive)

Layered keys
• Turns everything that we know about the registry upside down
• Every key node is now part of two trees in different dimensions– Operations like rename have to basically think in 3D• A key is not represented by a single key: it's now a key stack• The existence of a key node doesn't mean that it exists: see Tombstones• The absence of a key node doesn't mean that it doesn't exist:

see Merge-unbacked semantics

So how do they all
work together?

Predefined
Keys

Registry
Virtualization

LayeredKeys

Transactions
Symbolic
Links

Cross-feature bugs
• GPZ-2359: Windows Kernel use-after-free due to bad handling of predefined keys in

NtNotifyChangeMultipleKeys• GPZ-2366: Windows Kernel memory corruption due to insufficient handling of predefined keys in registry

virtualization• GPZ-2375: Windows Kernel multiple issues in the key replication feature of registry virtualization• GPZ-2389: Windows Kernel registry virtualization incompatible with transactions, leading to inconsistent

hive state and memory corruption• GPZ-2445: Windows Kernel arbitrary read by accessing predefined keys through differencing hives• GPZ-2446: Windows Kernel may reference unbacked layered keys through registry virtualization• GPZ-2447: Windows Kernel may reference rolled-back transacted keys through differencing hives• GPZ-2452: Windows Kernel CmDeleteLayeredKey may delete predefined tombstone keys, leading tosecurity descriptor UAF

Case study: Predefined keys vs the world• Predefined keys are supposed to be rejected by almost all syscalls• Otherwise, internal kernel functions are unaware of their semantics

and will usually crash when operating on them

• It's necessary to use a safe wrapper to filter them out whilereferencing a key handle: CmObReferenceObjectByHandle• Did it cover all potential scenarios?

Case study: Predefined keys vs the world

Date

GPZ#

Description

Case study: Predefined keys vs the world

Date

GPZ#

Description

September 2022

2359

NtNotifyChangeMultipleKeys doesn't go through the safe wrapper

Case study: Predefined keys vs the world

Date

GPZ#

Description

September 2022

2359

NtNotifyChangeMultipleKeys doesn't go through the safe wrapper

Case study: Predefined keys vs the world

Date

GPZ#

Description

September 2022

2359

NtNotifyChangeMultipleKeys doesn't go through the safe wrapper

October 2022

2366

Registry virtualization doesn't go through the safe wrapper

Case study: Predefined keys vs the world

Date

GPZ#

Description

September 2022

2359

NtNotifyChangeMultipleKeys doesn't go through the safe wrapper

October 2022

2366

Registry virtualization doesn't go through the safe wrapper

Case study: Predefined keys vs the world

Date

GPZ#

Description

September 2022

2359

NtNotifyChangeMultipleKeys doesn't go through the safe wrapper

October 2022

2366

Registry virtualization doesn't go through the safe wrapper

October 2022

2375

Registry virtualization uses unsafe CmpRebuildKcbCache which doesn't refresh the cache ofpredefined keys

Case study: Predefined keys vs the world

Date

GPZ#

Description

September 2022

2359

NtNotifyChangeMultipleKeys doesn't go through the safe wrapper

October 2022

2366

Registry virtualization doesn't go through the safe wrapper

October 2022

2375

Registry virtualization uses unsafe CmpRebuildKcbCache which doesn't refresh the cache ofpredefined keys

Case study: Predefined keys vs the world

Date

GPZ#

Description

September 2022

2359

NtNotifyChangeMultipleKeys doesn't go through the safe wrapper

October 2022

2366

Registry virtualization doesn't go through the safe wrapper

October 2022

2375

Registry virtualization uses unsafe CmpRebuildKcbCache which doesn't refresh the cache ofpredefined keys

April 2023

2445

The "safe" wrappers CmObReferenceObjectByHandle and CmObReferenceObjectByName
are insufficient for layered keys

Case study: Predefined keys vs the world

Date

GPZ#

Description

September 2022

2359

NtNotifyChangeMultipleKeys doesn't go through the safe wrapper

October 2022

2366

Registry virtualization doesn't go through the safe wrapper

October 2022

2375

Registry virtualization uses unsafe CmpRebuildKcbCache which doesn't refresh the cache ofpredefined keys

April 2023

2445

The "safe" wrappers CmObReferenceObjectByHandle and CmObReferenceObjectByName
are insufficient for layered keys

May 2023

2452

CmDeleteLayeredKey bypasses safe wrappers by directly freeing predefined keys via
CmpFreeKeyByCell

The ultimate fix• Finally, GPZ-2445 and GPZ-2452 were fixed in July 2023 by deprecatingpredefined keys completely– Flag 0x40 is cleared in CmpCheckKey for every key while loading a hive

• Great to see, as the feature is probably hardly used but was the sourceof many bugs and much confusionTesting, reproducing, reportingTesting

It's a bug!

It's a real vuln whencombined with these twoother mechanisms

Wait...

What is thiscode?

Three months pass

Hmm no, nota bugYeah, it's a bug,but not reachable

Testing• This is where you really learn how the registry works• So many moving pieces that it's almost impossible to be sure of any

behavior before testing it• Tooling:– Virtual machines + WinDbg: the !reg extensions are great– RegEdit, Process Monitor, Process Explorer– Own, custom tools for more advanced stuff: creating symlinks, loading
differencing hives etc.

Reproducing the bugs
• I try to trigger a system bugcheck / obvious security violation forevery bug– Crashes typically don't just happen, deliberate action is needed todemonstrate the corruption• Registry API is well documented, so writing C++ reproducers is smooth• Crafting semi-well but unusually formatted hives was the moredifficult part

– Hard to find existing tooling for my specific needs– Ended up manually patching the built-in Offline Registry Library (offreg.dll)
to produce most of my binary hive PoCs

Reporting

All security bugs filed in
the Project Zero bugtracker and submitted
to MSRCAll reported bugs
successfully fixed within
90+14 days so far

Average time to fix from
report until patch publicly
available: 81 days

Verifying fixes• An optional step, but I try to keep track of all registry changes on amonthly basis• There's a lot to learn– See if the fixes were correct/complete– See which avenue was taken – point fix, global code refactoring, something
in between?
– See if any lesser bugs mentioned in the reports were addressed– See if any internally found variants I was unaware of were patched– See if any (un)related functional changes were made• Found out about some good work this way– Attack surface reduction: KTM transactions, transacted renames,
predefined keys

– Code hardening: Integer overflow checks for security refcounts, rejecting cellindex -1 in cell translation code

Bonus: Exploitation

Exploitation• A huge subject on its own• Depends largely on the type of bugs and initial primitives– Logic issues: usually easiest and most reliable to exploit, but not that many ofthem in this research (did James find them all?)– Pool-based memory corruption: state-of-the-art exploitation techniques apply– Hive-based memory corruption: an unexplored class of issues worth
investigating further

Registry hive layout
Block boundaries

Base block

Empty bin

Vol 1

Root

Bin 1
Key cell (key node)

Subkey-list cell

Value cellFree space

Value-list cell
Source: Windows Internals, 7th Edition, Part 2 (A. Allievi, A. Ionescu, M. Russinovich, D. Solomon)

Bin 2

Sub
Vol 2
Key

Comparing to heap/pool allocators

Similarities• Divided into contiguous "free" and"allocated" cells, which are arbitrarily sized
chunks of data

• Some cells have a predefined structure(e.g. key nodes), while others contain 100%
user-specified data (e.g. value data)

Differences• Exact same data layout maintained on disk

and in memory• No randomization (100% deterministic) or any

protection against temporal/spatial violations• Most bugs allow reliably replacing/corrupting

arbitrary objects in the hive

So what do we corrupt?
• Corrupting our own hive gets us nowhere• At first glance, there are no pointers or anything to take us "outside" of the hive

• The solution: cell indexes– 32-bit unsigned integers used to reference cells between each other– On disk: simple offsets within the hive file
– In memory: offsets into a multi-level page table-like structure (cell maps)– At runtime, the HvpGetCellPaged function is used for the translation:

HvpGetCellPaged(uint32 CellIndex) → void* VirtualAddress

Cell maps and cell indexes
Cell index
Director index

Table index

Byte offset

32

0
Hive’s cell map directory
0

Cell map table

Target block

0
Cell

1023
511

Hive cell map directory pointerSource: Windows Internals, 7th Edition, Part 2 (A. Allievi, A. Ionescu, M. Russinovich, D. Solomon)

Out-of-bounds cell indexes• Due to how cell maps are allocated, an OOB index can be abused to point to:– An arbitrary address– Specific objects in memory– Itself (self-referential cell index)• Not quite Turing-complete, but firmly in the category of a "weird machine"• Provides an address leak and arbitrary read/write, all with one bug• Enables a reliable, data-only LPE attack

Takeaways• The registry is a fascinating research target, but has been publicly

underexplored throughout its history• If you're a researcher: deep, persistent analysis pays off• If you're a software vendor: attack surface reduction, eliminationof entire bug classes and well-placed mitigations have an
outsized impact on security© Copyright Microsoft Corporation. All rights reserved.