Security descriptors, part3: raw descriptors and PowerShell

<< Part 2

In this part I'll get to the manipulation of the security descriptors with PowerShell. I'll deal with the code a bit differently than in the previous part: the whole code is attached as a file, and I show only the examples of the use and the highlights of the implementation in the post.

As I've mentioned in Part 1, there are multiple formats used for the descriptors. I needed to deal with the security descriptors for the ETW logger sessions. These descriptors happen to be stored in the serialized binary format that I wanted to be able to print, manipulate, and put back. Which first of all means the conversion between the binary format, something human-readable to read them conveniently, SDDL, and the .NET classes. If you remember the Part1, there are two kinds of classes: Raw and Common, and Common is the more high-level one but chokes on the ACLs that are not in the canonical form. Whenever possible (i.e. whenever their APIs are the same), I've tried to support both classes but where they're different, I went for only the Raw class for the first cut. Since you never know, what would be in the descriptors you read, choking on a non-canonical descriptor would be a bad thing.

First, let me show, how to get the security descriptor for a particular ETW session defined in the Autologger. It starts with finding the GUID of the session:

PS C:\windows\system32> $regwmi = "hklm:\SYSTEM\CurrentControlSet\Control\WMI"
PS C:\windows\system32> $session = "SetupPlatform"
PS C:\windows\system32> $guid = (Get-ItemProperty -LiteralPath "$regwmi\Autologger\$session" -Name Guid -ErrorAction SilentlyContinue).Guid -replace "^{(.*)}",'$1'
PS C:\windows\system32> $guid
16A66B02-B884-465E-92F1-2B0ABB86C4D7

The curly braces around the GUID had to be removed to fit the next step. The session GUIDs seem to be pretty stable for the pre-defined sessions, though if the sessions change much between the Windows versions, the GUIDs would also change. The same approach works for the ETW providers, just get the GUID, and from there on managing the permissions for a provider is the same as for a session.

The next step is to get the descriptor itself, it's stored in Registry in the serialized byte format:

PS C:\windows\system32> $bytes = (Get-Item "$regwmi\Security").GetValue($guid)
PS C:\windows\system32> $bytes
PS C:\windows\system32>

Uh-oh, it's empty. Well, if there is no explicit descriptor for a GUID, a default descriptor is used, one with the GUID 0811c1af-7a07-4a06-82ed-869455cdf713:

PS C:\windows\system32> if ($bytes -eq $null) { $bytes = (Get-Item "$regwmi\Security").GetValue("0811c1af-7a07-4a06-82ed-869455cdf713") }
PS C:\windows\system32> $bytes
...(prints a lot of bytes)...

Got the data. Now let's make sense of it by converting it to a raw descriptor object using a function from the attached module:

PS C:\windows\system32> Import-Module Security.psm1
PS C:\windows\system32> $sd = ConvertTo-RawSd $bytes
PS C:\windows\system32> $sd
ControlFlags : DiscretionaryAclPresent, SelfRelative
Owner : S-1-5-32-544
Group : S-1-5-32-544
SystemAcl :
DiscretionaryAcl : {System.Security.AccessControl.CommonAce, System.Security.AccessControl.CommonAce,
                         System.Security.AccessControl.CommonAce, System.Security.AccessControl.CommonAce...}
ResourceManagerControl : 0
BinaryLength : 236

Just like the functions for the principals that I've shown in Part 2, ConvertTo-RawSd takes the descriptor in whatever acceptable format (a byte array, an SDDL string or another security descriptor object) and converts it to the Raw object. Incidentally, it can be used to copy the existing descriptor objects. Under the hood it works like this:

    if ($Sd -is [System.Security.AccessControl.GenericSecurityDescriptor]) {
        $Sd = (ConvertTo-BytesSd $Sd).Bytes
    }

    if ($Sd -is [Byte[]]) {
        New-Object System.Security.AccessControl.RawSecurityDescriptor @($Sd, 0)
    } elseif ($Sd -is [string]) {
        New-Object System.Security.AccessControl.RawSecurityDescriptor @($Sd)
    }

Just writing the $sd to the output gives some idea of the contents. It can also be printed as an SDDL string with another helper function:

PS C:\windows\system32> ConvertTo-Sddl $sd
O:BAG:BAD:(A;;0x800;;;WD)(A;;0x120fff;;;SY)(A;;0x120fff;;;LS)(A;;0x120fff;;;NS)(A;;0x120fff;;;BA)(A;;0xee5;;;LU)(A;;LC;;;MU)(A;;0x800;;;AC)
PS C:\windows\system32> ConvertTo-Sddl $bytes
O:BAG:BAD:(A;;0x800;;;WD)(A;;0x120fff;;;SY)(A;;0x120fff;;;LS)(A;;0x120fff;;;NS)(A;;0x120fff;;;BA)(A;;0xee5;;;LU)(A;;LC;;;MU)(A;;0x800;;;AC)

As you can see, it also accepts the descriptor in whatever format and converts it to SDDL. Under the hood, it works by first converting its argument to the raw descriptor and then getting the SDDL from it with

$Sd.GetSddlForm("All")

To print the ACL information in a more human-readable form, you can use:

PS C:\windows\system32> ConvertTo-PrintSd $sd
Owner: BUILTIN\Administrators
Group: BUILTIN\Administrators
 AccessAllowed Everyone 0x00000800 [None]
 AccessAllowed NT AUTHORITY\SYSTEM 0x00120fff [None]
 AccessAllowed NT AUTHORITY\LOCAL SERVICE 0x00120fff [None]
 AccessAllowed NT AUTHORITY\NETWORK SERVICE 0x00120fff [None]
 AccessAllowed BUILTIN\Administrators 0x00120fff [None]
 AccessAllowed BUILTIN\Performance Log Users 0x00000ee5 [None]
 AccessAllowed BUILTIN\Performance Monitor Users 0x00000004 [None]
 AccessAllowed APPLICATION PACKAGE AUTHORITY\ALL APPLICATION PACKAGES 0x00000800 [None]

PS C:\windows\system32> ConvertTo-PrintSd $sd -Trace
Owner: BUILTIN\Administrators
Group: BUILTIN\Administrators
 AccessAllowed Everyone 0x00000800=RegisterGuids [None]
 AccessAllowed NT AUTHORITY\SYSTEM 0x00120fff=Query, Set, Notification, ReadDescription, Execute, CreateRealtime, CreateOndisk, Enable, AccessKernelLogger, LogEvent, AccessRealtime, RegisterGuids, FullControl [None]
 AccessAllowed NT AUTHORITY\LOCAL SERVICE 0x00120fff=Query, Set, Notification, ReadDescription, Execute, CreateRealtime, CreateOndisk, Enable, AccessKernelLogger, LogEvent, AccessRealtime, RegisterGuids, FullControl [None]
 AccessAllowed NT AUTHORITY\NETWORK SERVICE 0x00120fff=Query, Set, Notification, ReadDescription, Execute, CreateRealtime, CreateOndisk, Enable, AccessKernelLogger, LogEvent, AccessRealtime, RegisterGuids, FullControl [None]
 AccessAllowed BUILTIN\Administrators 0x00120fff=Query, Set, Notification, ReadDescription, Execute, CreateRealtime, CreateOndisk, Enable, AccessKernelLogger, LogEvent, AccessRealtime, RegisterGuids, FullControl [None]
 AccessAllowed BUILTIN\Performance Log Users 0x00000ee5=Query, Notification, CreateRealtime, CreateOndisk, Enable, LogEvent, AccessRealtime, RegisterGuids [None]
 AccessAllowed BUILTIN\Performance Monitor Users 0x00000004=Notification [None]
 AccessAllowed APPLICATION PACKAGE AUTHORITY\ALL APPLICATION PACKAGES 0x00000800=RegisterGuids [None]

 As usual, it accepts the descriptors in whatever format. It shows the owner and group, and the discretionary ACL that controls the access to the object. Each ACL entry contains the type of the entry (allowed/denied), name of the principal, the permissions bitmask, and the inheritance flags in the square brackets (since there is no inheritance for the ETW stuff, all of them are [None] here). With a bit of extra help, it can print the permission bits in the symbolic form as well. Remember, each kind of object that uses ACLs has its own meaning for the same bits in the mask. So to print these bits symbolically, you've got to tell it, which kind of object this security descriptor applies to. The switch -Trace selects the ETW objects, and the other supported switches are -File, -Registry, -Crypto, -Event, -Mutex, and -Semaphore.

Suppose now, we want to check the permissions granted to BUILTIN\Administrators. Another helper function to the rescue:

PS C:\windows\system32> $allowMask, $denyMask = Get-SidAccess "BUILTIN\Administrators" $sd
PS C:\windows\system32> "{0:X8}" -f $allowMask
00120FFF
PS C:\windows\system32> "{0:X8}" -f $denyMask
00000000

Like the other functions, it takes a principal (user or group) and a descriptor in whatever format. It returns two values, the permission bits that are allowed and those that are denied. Since the denials are usually applied first, if you want to check, which bits are really allowed, you need to do a bit more masking:

PS C:\windows\system32> $allowMask = $allowMask -band -bnot $denyMask

Internally, Get-SidAccess works by going through all the entries in the ACL and collecting two masks from all the entries with the matching principal (i.e. SID).

And you can grant a permission:

PS C:\windows\system32> Grant-To -Sid "NT SERVICE\EventLog" -Mask 0xE3 -Object $sd
PS C:\windows\system32> ConvertTo-PrintSd $sd
Owner: BUILTIN\Administrators
Group: BUILTIN\Administrators
 AccessAllowed Everyone 0x00000800 [None]
 AccessAllowed NT AUTHORITY\SYSTEM 0x00120fff [None]
 AccessAllowed NT AUTHORITY\LOCAL SERVICE 0x00120fff [None]
 AccessAllowed NT AUTHORITY\NETWORK SERVICE 0x00120fff [None]
 AccessAllowed BUILTIN\Administrators 0x00120fff [None]
 AccessAllowed BUILTIN\Performance Log Users 0x00000ee5 [None]
 AccessAllowed BUILTIN\Performance Monitor Users 0x00000004 [None]
 AccessAllowed APPLICATION PACKAGE AUTHORITY\ALL APPLICATION PACKAGES 0x00000800 [None]
 AccessAllowed NT SERVICE\EventLog 0x000000e3 [None]

Grant-To works only with the Raw descriptors or Raw ACLs as object. Unlike the other functions, the object (or possibly multiple objects in a list) gets modified in-place.Well, maybe it would get extended to produce the new objects too but for now it has this limitation. Grant-To is smart enough to add a new ACE if it doesn't find an existing one. And it's smart enough to automatically remove these bits from the Deny ACL if one is present. It can also be used to revoke the permissions:

PS C:\windows\system32> Grant-To -Revoke -Sid "NT SERVICE\EventLog" -Mask 0x3 -Object $sd
PS C:\windows\system32> ConvertTo-PrintSd $sd
Owner: BUILTIN\Administrators
Group: BUILTIN\Administrators
 AccessAllowed Everyone 0x00000800 [None]
 AccessAllowed NT AUTHORITY\SYSTEM 0x00120fff [None]
 AccessAllowed NT AUTHORITY\LOCAL SERVICE 0x00120fff [None]
 AccessAllowed NT AUTHORITY\NETWORK SERVICE 0x00120fff [None]
 AccessAllowed BUILTIN\Administrators 0x00120fff [None]
 AccessAllowed BUILTIN\Performance Log Users 0x00000ee5 [None]
 AccessAllowed BUILTIN\Performance Monitor Users 0x00000004 [None]
 AccessAllowed APPLICATION PACKAGE AUTHORITY\ALL APPLICATION PACKAGES 0x00000800 [None]
 AccessAllowed NT SERVICE\EventLog 0x000000e0 [None]
 

And to deny the permissions:

PS C:\windows\system32> Grant-To -Deny -Sid "NT SERVICE\EventLog" -Mask 0xc0 -Object $sd
PS C:\windows\system32> ConvertTo-PrintSd $sd
Owner: BUILTIN\Administrators
Group: BUILTIN\Administrators
 AccessDenied NT SERVICE\EventLog 0x000000c0 [None]
 AccessAllowed Everyone 0x00000800 [None]
 AccessAllowed NT AUTHORITY\SYSTEM 0x00120fff [None]
 AccessAllowed NT AUTHORITY\LOCAL SERVICE 0x00120fff [None]
 AccessAllowed NT AUTHORITY\NETWORK SERVICE 0x00120fff [None]
 AccessAllowed BUILTIN\Administrators 0x00120fff [None]
 AccessAllowed BUILTIN\Performance Log Users 0x00000ee5 [None]
 AccessAllowed BUILTIN\Performance Monitor Users 0x00000004 [None]
 AccessAllowed APPLICATION PACKAGE AUTHORITY\ALL APPLICATION PACKAGES 0x00000800 [None]
 AccessAllowed NT SERVICE\EventLog 0x00000020 [None]

The difference between -Revoke and -Deny is that -Revoke simply removes the permissions from the Allow mask, while -Deny also adds them to the Deny mask. Note that the Deny entries get added in the front, to keep the order of the entries canonical. The bits that get set in the Deny mask, get reset in the Allow mask. The bits can be revoked from the Deny mask as well:

PS C:\windows\system32> Grant-To -Revoke -Deny -Sid "NT SERVICE\EventLog" -Mask 0xF0 -Object $sd
PS C:\windows\system32> ConvertTo-PrintSd $sd
Owner: BUILTIN\Administrators
Group: BUILTIN\Administrators
 AccessAllowed Everyone 0x00000800 [None]
 AccessAllowed NT AUTHORITY\SYSTEM 0x00120fff [None]
 AccessAllowed NT AUTHORITY\LOCAL SERVICE 0x00120fff [None]
 AccessAllowed NT AUTHORITY\NETWORK SERVICE 0x00120fff [None]
 AccessAllowed BUILTIN\Administrators 0x00120fff [None]
 AccessAllowed BUILTIN\Performance Log Users 0x00000ee5 [None]
 AccessAllowed BUILTIN\Performance Monitor Users 0x00000004 [None]
 AccessAllowed APPLICATION PACKAGE AUTHORITY\ALL APPLICATION PACKAGES 0x00000800 [None]
 AccessAllowed NT SERVICE\EventLog 0x00000020 [None]

Since the Deny mask became 0, Grant-to was smart enough to remove it altogether. It's also possible to just set the mask instead of manipulating the individual bits:

PS C:\windows\system32> Grant-To -Sid "NT SERVICE\EventLog" -SetMask 0x0F -Object $sd
PS C:\windows\system32> ConvertTo-PrintSd $sd
Owner: BUILTIN\Administrators
Group: BUILTIN\Administrators
 AccessAllowed Everyone 0x00000800 [None]
 AccessAllowed NT AUTHORITY\SYSTEM 0x00120fff [None]
 AccessAllowed NT AUTHORITY\LOCAL SERVICE 0x00120fff [None]
 AccessAllowed NT AUTHORITY\NETWORK SERVICE 0x00120fff [None]
 AccessAllowed BUILTIN\Administrators 0x00120fff [None]
 AccessAllowed BUILTIN\Performance Log Users 0x00000ee5 [None]
 AccessAllowed BUILTIN\Performance Monitor Users 0x00000004 [None]
 AccessAllowed APPLICATION PACKAGE AUTHORITY\ALL APPLICATION PACKAGES 0x00000800 [None]
 AccessAllowed NT SERVICE\EventLog 0x0000000f [None]

When setting the mask, it still updates the opposite mask to be sensible. The switch -Simple can be used with -SetMask (but not with -Mask) to skip this smartness.

As you can see, Grant-To is pretty smart in dealing with many things. One thing it's not smart enough to handle is the inheritance bits in the ACL entries. It has no way to specify them, and just leaves them as-is in the entries it modifies. The use of inheritance also means that there might be multiple Allow entries and multiple Deny entries on the same ACL for the same principal, with the different inheritance flags. Grant-To can't handle this well either. It only knows about the first entry of either type. Since I was really interested in the ETW objects that don't use the inheritance, I didn't care much for now.

It also can't deal with the Common security descriptors (other than through a manual conversion to the Raw descriptors and back). The Common descriptors have their own similar functionality, their ACLs are represented with the class DiscretionaryAcl that doesn't allow the direct messing with it but has the methods SetAccess() and RemoveAccess() that do the similar thing, and do support the inheritance bits, but with the less obvious arguments.

Under the hood, Grant-To iterates over the ACL entries, finding the interesting ones:

if ($acl[$i].SecurityIdentifier -eq $sid) {
    if ($acl[$i].AceQualifier -eq $qual) {

If the ACE is not found, it gets inserted (yes, it's a CommonAce object even in the Raw ACL):

$ace = New-Object System.Security.AccessControl.CommonAce @(0, $qual[$j], 0, $sid, $false, $null)
$acl.InsertAce($idx, $ace)

If the mask in an ACE becomes 0, the ACE gets deleted:

$acl.RemoveAce($idx)

And the mask in the ACEs gets manipulated directly:

$ace.AccessMask = $ace.AccessMask -bor $Mask

Returning back to the task of modifying the descriptors for the ETW sessions, the last step is to set the modified ACLs:

PS C:\windows\system32> $bytes = (ConvertTo-BytesSd $sd).Bytes
PS C:\windows\system32> Set-ItemProperty -LiteralPath "$regwmi\Security" -Name $guid -Value $bytes -Type BINARY

The ConvertTo-BytesSd converts the descriptor to the serialized bytes, from whatever format (including the Common descriptor object). It uses an interesting way to return the value. It needs to return a value of type "Byte[]" but any array returned from a PowerShell function becomes disassembled and reassembled as an array of type "Object[]", which then doesn't work right. To work around that, it returns a hashtable with the array of bytes in an entry:

@{ Bytes = $o; }

And then the caller extracts the value from the hashtable unmolested, and the byte array can be set in the registry.

If you wonder, what's magical about the bitmask 0xE3, it's the value that allows to query the information about a running ETW session and modify its modes.

<< Part 2

Security.psm1