# Windows Registry Security: DACLs, Access Tokens, and Privileged Services

> Understand how Windows authorizes registry access, how key handles preserve granted rights, and why privileged services can cross a caller boundary.

- Published: 2026-09-04
- Difficulty: advanced
- Series: Windows Service Internals
- Tags: Windows, Windows Internals, Registry, Access Control, Security Architecture
- Source: https://stevenfoerster.com/tutorials/windows-registry-security-access-tokens-dacls/

## Prerequisites

- Familiarity with Windows users, groups, and SIDs
- Basic understanding of Win32 handles and access masks

The Windows registry is not a configuration text file with a permissions layer bolted on top. Registry keys are securable kernel-managed objects. Opening a key is an access request evaluated against a security descriptor, and the returned handle carries only the rights Windows granted at that moment.

That model is reliable, but it is easy to ask it the wrong question.

Suppose an ordinary user cannot open a machine key for writing. The user calls a LocalSystem service, and the service opens the same key without impersonating the caller. The open now succeeds. The registry did not bypass its DACL. It evaluated a different token: LocalSystem instead of the user.

This tutorial builds the registry security model from the key's security descriptor through the caller's token and the handle's granted access mask. It then shows where privileged services must add an authorization decision of their own.

## Registry keys are securable objects

A registry key can have a security descriptor containing:

- An owner SID
- A primary group SID
- A discretionary access control list (DACL)
- A system access control list (SACL)
- Control flags describing inheritance and protection

The DACL determines which trustees are allowed or denied specific access rights. The SACL controls auditing and mandatory labels rather than ordinary discretionary grants.

Child keys normally inherit access-control entries from their parent unless their security descriptor protects the DACL from inheritance or supplies a different explicit policy.

Windows exposes the descriptor through APIs such as `RegGetKeySecurity`, `GetSecurityInfo`, and `GetNamedSecurityInfo`. PowerShell provides a convenient read-only view:

```powershell
$key = 'Registry::HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion'
$acl = Get-Acl -LiteralPath $key

$acl.Owner
$acl.Sddl
$acl.Access | Format-Table IdentityReference, RegistryRights, AccessControlType, IsInherited
```

The friendly `RegistryRights` rendering is useful, but SDDL is the more compact representation to preserve in diagnostics. It keeps owner, group, DACL, SACL, inheritance flags, and ACE ordering together.

## The registry-specific access mask

Registry keys combine standard object rights with registry-specific rights.

| Right | Value | Meaning |
| --- | ---: | --- |
| `KEY_QUERY_VALUE` | `0x0001` | Read a value from the key |
| `KEY_SET_VALUE` | `0x0002` | Create, replace, or delete a value |
| `KEY_CREATE_SUB_KEY` | `0x0004` | Create a child key |
| `KEY_ENUMERATE_SUB_KEYS` | `0x0008` | Enumerate child keys |
| `KEY_NOTIFY` | `0x0010` | Request change notifications |
| `KEY_CREATE_LINK` | `0x0020` | Create a symbolic-link key, reserved for system use |
| `DELETE` | `0x00010000` | Delete the key when other deletion conditions are satisfied |
| `READ_CONTROL` | `0x00020000` | Read the security descriptor, excluding the SACL |
| `WRITE_DAC` | `0x00040000` | Change the DACL |
| `WRITE_OWNER` | `0x00080000` | Change the owner |

The common composites are:

```c
KEY_READ = (STANDARD_RIGHTS_READ   |
            KEY_QUERY_VALUE        |
            KEY_ENUMERATE_SUB_KEYS |
            KEY_NOTIFY) &
           ~SYNCHRONIZE;

KEY_WRITE = (STANDARD_RIGHTS_WRITE |
             KEY_SET_VALUE         |
             KEY_CREATE_SUB_KEY) &
            ~SYNCHRONIZE;
```

Registry keys do not support the `SYNCHRONIZE` standard right, so the formal composite definitions mask it out. `KEY_ALL_ACCESS` includes the registry-specific rights plus the applicable standard rights. Asking for it by habit is poor practice. Request the minimum rights the operation needs.

The WOW64 flags, `KEY_WOW64_32KEY` and `KEY_WOW64_64KEY`, select a registry view. They do not grant read or write authority. Combine one with the actual access request when a 32-bit process must open the 64-bit view or vice versa.

## Access is decided when the handle is opened

An open call supplies a desired access mask:

```c
HKEY key = NULL;
LONG result = RegOpenKeyExW(
    HKEY_LOCAL_MACHINE,
    L"SOFTWARE\\ExampleCorp\\MachinePolicy",
    0,
    KEY_QUERY_VALUE,
    &key
);
```

Windows evaluates the desired mask against the key's security descriptor and the caller's effective token. On success, the handle records the granted access.

Many later operations check the handle's granted rights. `RegQueryValueExW` needs a handle with `KEY_QUERY_VALUE`, and `RegSetValueExW` specifically requires that the key was opened with `KEY_SET_VALUE`.

Key creation is the documented exception, and it matters more than it sounds. Microsoft states that certain registry operations perform their access check against the security descriptor of the key rather than against the mask the handle was opened with: a key opened with `KEY_READ` can still be used to create subkeys if the key's security descriptor permits it. So "the handle was only opened for read" is a reliable constraint on value writes and not a reliable constraint on subkey creation.

This has two practical consequences.

First, a token change does not upgrade an existing handle. If a service opens a key for read while impersonating a user, then reverts to LocalSystem, that handle still has only the access granted at open time.

Second, a privileged handle should not be handed to less trusted code. If the service opens a key with `KEY_ALL_ACCESS` as LocalSystem and passes the handle to a plugin or another process, the recipient can use the rights already present on the handle even if its own token could not open the key independently.

```mermaid
flowchart LR
    T[Effective token at open] --> A[Access check against key DACL]
    D[Desired access mask] --> A
    A -->|All requested rights, or the call fails| H[Key handle with granted rights]
    H --> Q[Most later operations check handle rights]
    H --> C[Key creation re-checks the key security descriptor]
```

An explicit desired mask is all or nothing: Windows grants every right requested or fails the open with `ACCESS_DENIED`. A handle carrying a subset of what was asked for comes from requesting `MAXIMUM_ALLOWED`, which resolves to whatever the token is actually entitled to.

> [!note]
> Handles are references to an opened object with a granted access mask. They are not path strings that cause Windows to re-resolve the full DACL on every call. The registry qualifies that rule rather than following it absolutely, so confirm which operations honor the handle mask before relying on a read-only handle as a security boundary.

## How a DACL is evaluated

A DACL is an ordered list of access control entries (ACEs). Each ACE identifies a trustee SID, an allow or deny type, inheritance behavior, and an access mask.

A simplified SDDL fragment might be:

```text
D:
  (A;CI;KR;;;BU)
  (A;CI;KA;;;BA)
  (A;CI;KA;;;SY)
```

Read conceptually:

- Built-in Users (`BU`) receive key read (`KR`)
- Built-in Administrators (`BA`) receive key all access (`KA`)
- LocalSystem (`SY`) receives key all access
- `CI` allows the ACE to inherit to child containers

The actual access check also accounts for token group attributes, restricted SIDs, privileges, ownership rules, mandatory integrity policy, object type, and generic-right mapping. ACE order matters, especially when explicit deny entries are present.

Avoid implementing a DACL evaluator by looping over ACEs yourself. Use the object manager by opening the object as the caller, or use `AccessCheck` or the Authz API with the correct token and generic mapping.

## Which token does the registry see?

For a normal registry open, Windows uses the calling thread's effective security context:

```text
thread has an impersonation token -> evaluate that token
thread has no impersonation token -> evaluate the process primary token
```

This is why the same code can receive different results without the key's DACL changing.

```c
LONG TryOpenMachinePolicy(void) {
    HKEY key = NULL;
    LONG status = RegOpenKeyExW(
        HKEY_LOCAL_MACHINE,
        L"SOFTWARE\\ExampleCorp\\MachinePolicy",
        0,
        KEY_SET_VALUE,
        &key
    );
    if (status == ERROR_SUCCESS) RegCloseKey(key);
    return status;
}
```

Run from a standard-user process, the function may return `ERROR_ACCESS_DENIED`. Run on a LocalSystem service thread, it may return `ERROR_SUCCESS`. Run on that same service thread while it impersonates the standard user, it should again return `ERROR_ACCESS_DENIED` if the DACL gives Users read-only access.

The companion tutorial [Windows Service Impersonation](https://stevenfoerster.com/tutorials/windows-service-impersonation-and-access-tokens/) follows that token transition in detail.

## Registry ACLs do not authorize service requests

Consider a service method:

```c
DWORD RpcSetMachinePolicy(const wchar_t *name, DWORD value) {
    HKEY key = NULL;
    LONG status = RegOpenKeyExW(
        HKEY_LOCAL_MACHINE,
        L"SOFTWARE\\ExampleCorp\\MachinePolicy",
        0,
        KEY_SET_VALUE,
        &key
    );
    if (status != ERROR_SUCCESS) return status;

    status = RegSetValueExW(
        key,
        name,
        0,
        REG_DWORD,
        (const BYTE *)&value,
        sizeof(value)
    );
    RegCloseKey(key);
    return status;
}
```

If this handler runs in a LocalSystem service and never impersonates or authorizes the caller, the key's DACL sees only LocalSystem. An ordinary user has indirectly gained whatever mutation the method exposes.

The service has three broad design choices:

1. **Impersonate before opening the key.** Use the registry DACL as the operation policy.
2. **Evaluate the caller explicitly, then revert and write as the service.** Use when authorized callers need the service to perform an operation they cannot do directly.
3. **Expose no caller-controlled mutation.** Keep machine policy internal to trusted service workflows.

The wrong choice is accidental delegation: accept caller-controlled names or data, perform the mutation as LocalSystem, and assume the registry ACL protects the path.

## Creating keys and inherited security

`RegCreateKeyExW` can receive a `SECURITY_ATTRIBUTES` structure for a newly created key. If no explicit security descriptor is supplied, the new key normally inherits a default descriptor from its parent.

Here `policyDescriptor` is a `PSECURITY_DESCRIPTOR` the caller has already built, for example with `ConvertStringSecurityDescriptorToSecurityDescriptorW` from an SDDL string:

```c
PSECURITY_DESCRIPTOR policyDescriptor = NULL;
ConvertStringSecurityDescriptorToSecurityDescriptorW(
    L"D:(A;CI;KR;;;BU)(A;CI;KA;;;BA)(A;CI;KA;;;SY)",
    SDDL_REVISION_1,
    &policyDescriptor,
    NULL
);

SECURITY_ATTRIBUTES attributes = {
    .nLength = sizeof(attributes),
    .lpSecurityDescriptor = policyDescriptor,
    .bInheritHandle = FALSE
};

DWORD disposition = 0;
HKEY child = NULL;
LONG status = RegCreateKeyExW(
    parent,
    L"PolicyV2",
    0,
    NULL,
    REG_OPTION_NON_VOLATILE,
    KEY_READ | KEY_WRITE,
    &attributes,
    &child,
    &disposition
);
```

`disposition` tells the caller whether the key was created or an existing key was opened. Code that assumes "success means I created a fresh key with my descriptor" can make incorrect security assumptions when the key already existed.

Inheritance deserves explicit testing. A parent DACL can contain inheritable ACEs, non-inheritable ACEs, or ACEs limited to children. A child can also protect its DACL from further inheritance. Inspect both the parent and resulting child rather than inferring the child policy from one line of creation code.

## Changing a DACL is a separate authority

Writing values and rewriting permissions are different operations.

- `KEY_SET_VALUE` permits value mutation.
- `WRITE_DAC` permits DACL changes.
- `WRITE_OWNER` permits owner changes.
- `ACCESS_SYSTEM_SECURITY` plus the appropriate privilege is required to access a SACL.

An application that only needs to store configuration should not request `WRITE_DAC` or `WRITE_OWNER`. Separating these rights limits the damage from handle misuse.

Ownership can affect access because an owner has implicit ability to read and change the DACL under Windows access-control rules. Privileges such as `SeTakeOwnershipPrivilege`, `SeBackupPrivilege`, and `SeRestorePrivilege` can also alter normal behavior when enabled and used through APIs that honor them. These are exceptional administrative mechanisms, not substitutes for a correct DACL.

## Mandatory integrity is an additional policy

Windows Mandatory Integrity Control adds a label such as Low, Medium, High, or System to a token and securable object. A mandatory label ACE can restrict write-up or other operations even when the DACL would otherwise allow them.

The important word is **additional**. Integrity level does not replace discretionary authorization:

- Two unrelated users can both run at Medium integrity.
- A Medium-integrity token can still be denied by a DACL.
- A High-integrity administrator is not automatically the only principal allowed by an object policy.
- A service should not interpret "Medium or higher" as a general permission to mutate machine state.

Use integrity policy to enforce trust-level separation, and use DACLs or explicit authorization to identify which principals may perform which operations.

## The 32-bit and 64-bit registry views

On 64-bit Windows, some paths have separate logical views for 32-bit and 64-bit applications. A test can appear inconsistent when two processes opened different views.

Make the view explicit when it matters:

```c
RegOpenKeyExW(
    HKEY_LOCAL_MACHINE,
    L"SOFTWARE\\ExampleCorp",
    0,
    KEY_QUERY_VALUE | KEY_WOW64_64KEY,
    &key
);
```

When documenting a result, record:

- Process architecture
- Requested WOW64 flag
- Fully qualified hive and path
- Desired access mask
- Effective token identity and integrity level
- Returned status

Without those fields, "the registry call succeeded" is underspecified.

## A safe inspection exercise

This exercise reads descriptors and tests access by opening handles. It does not create, change, or delete registry data.

### 1. Inspect an existing key

```powershell
$path = 'Registry::HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion'
$acl = Get-Acl -LiteralPath $path

[pscustomobject]@{
    Owner = $acl.Owner
    Sddl  = $acl.Sddl
}

$acl.Access |
    Sort-Object IdentityReference |
    Format-Table IdentityReference, RegistryRights, AccessControlType, IsInherited
```

### 2. Open it with minimum read access

```powershell
$key = [Microsoft.Win32.Registry]::LocalMachine.OpenSubKey(
    'SOFTWARE\Microsoft\Windows NT\CurrentVersion',
    $false
)

$key.GetValue('ProductName')
$key.Dispose()
```

### 3. Use a non-mutating native access probe

A small native program can call `RegOpenKeyExW` with `KEY_SET_VALUE`, print the status, and close the handle if the open succeeds. It never calls a mutation API.

```c
HKEY key = NULL;
LONG status = RegOpenKeyExW(
    HKEY_LOCAL_MACHINE,
    L"SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion",
    0,
    KEY_SET_VALUE,
    &key
);

wprintf(L"RegOpenKeyExW(KEY_SET_VALUE) = %ld\n", status);
if (key != NULL) RegCloseKey(key);
```

Run the same binary from a standard-user process and an elevated administrative process. The exercise isolates the access decision without changing the key.

## Review checklist for registry-writing services

For every privileged registry operation, answer:

1. Is the full path fixed, partially caller-controlled, or fully caller-controlled?
2. Can the caller choose the value name, type, or data?
3. Which token is effective when the key is opened?
4. Which exact rights are requested?
5. Was the handle opened before or after impersonation?
6. Is the service relying on the key DACL or applying separate policy?
7. Can the operation create an unexpected parent or child key?
8. What security descriptor will a new key receive?
9. Which registry view is used?
10. Are rollback and audit behavior defined for partial failure?

The registry can enforce only the access request it receives. A correct service must ensure the right token makes that request.

## References

- [CVE-2026-69321: umpo.dll Power Dependency Coordinator analysis](https://stevenfoerster.com/notes/cve-2026-69321/), a worked example of a missing authorization check in a SYSTEM service's RPC handlers
- [Registry key security and access rights](https://learn.microsoft.com/en-us/windows/win32/sysinfo/registry-key-security-and-access-rights), Microsoft Learn
- [Access tokens](https://learn.microsoft.com/en-us/windows/win32/secauthz/access-tokens), Microsoft Learn
- [Requesting access rights to an object](https://learn.microsoft.com/en-us/windows/win32/secauthz/requesting-access-rights-to-an-object), Microsoft Learn
- [How DACLs control access to an object](https://learn.microsoft.com/en-us/windows/win32/secauthz/how-dacls-control-access-to-an-object), Microsoft Learn
- [`RegOpenKeyExW`](https://learn.microsoft.com/en-us/windows/win32/api/winreg/nf-winreg-regopenkeyexw), Microsoft Learn
- [`RegCreateKeyExW`](https://learn.microsoft.com/en-us/windows/win32/api/winreg/nf-winreg-regcreatekeyexw), Microsoft Learn
