Posts Tagged ‘Enumeration’

Have you ever tried to use PowerShell (or .NET) to mess with file or folder permissions and wondered what the ‘Synchronize’ right means? It pops up all over the place, like on existing ACEs:

what_does_synchronize_mean_example
And on new ACEs that you create (even if you don’t include it):

synchronize_2

If you try to check permissions using the ACL Editor, you won’t see it anywhere. Here’s the ACE for ‘Users’ from the ‘C:\powershell’ folder shown in the first screenshot above:

synchronize_3_acl_editor

So, what is this mysterious right, and why does PowerShell/.NET insist on showing it everywhere? Let’s start with the definition from MSDN:

The right to use the object for synchronization. This enables a thread to wait until the object is in the signaled state. Some object types do not support this access right.

The first time I read that, I didn’t think it sounded all that important. It turns out, though, that it’s critical for working with files and folders.

Before I explain a little bit more about why that right shows up, let’s briefly cover what makes up an access control entry’s access mask. It’s a 32-bit integer, which means that, theoretically, there are 32 different rights that can be controlled (32 bits means 32 different on/off switches). In practice, you don’t get that many rights, though. No matter what type of object you’re working with (file, folder, registry key, printer, service, AD object, etc), those 32-bits are broken down like this:

  • Bits 0-15 are used for object specific rights. These rights differ between object types, e.g., bit 1 for a file means ‘CreateFiles’, for a registry key means ‘SetValue’, and for an AD object means ‘DeleteChild’.
  • Bits 16-23 are used for “Standard access rights”. These rights are shared among the different types of securable objects, e.g., bit 16 corresponds to the right to delete the object, and it means the same thing for files, folders, registry keys, etc. As far as I know, only bits 16-20 in this range do anything.
  • Bit 24 controls access to the SACL.
  • Bits 25-27 are reserved and not currently used.
  • Bits 28-31 are “Generic access rights”. They are a shorthand way of specifying four common access masks: read, write, execute, and all (full control). These bits are translated into a combination of object specific and standard access rights, and the translation differs depending on the type of object the ACE belongs to.

The ‘Synchronize’ right is controlled by bit 20, so it’s one of the standard access rights:

PS> [math]::Log([System.Security.AccessControl.FileSystemRights]::Synchronize, 2)
20

If you manage to remove the right (or if you explicitly deny it), bad things will happen. For folders, you won’t be able to see child items. For files, you won’t be able to view the contents. It turns out some very important Win32 APIs require that right to be granted, at least for file and folder objects. You get a hint of it from this MSDN page:

Note that you cannot use an access-denied ACE to deny only GENERIC_READ or only GENERIC_WRITE access to a file. This is because for file objects, the generic mappings for both GENERIC_READ or GENERIC_WRITE include the SYNCHRONIZE access right. If an ACE denies GENERIC_WRITE access to a trustee, and the trustee requests GENERIC_READ access, the request will fail because the request implicitly includes SYNCHRONIZE access which is implicitly denied by the ACE, and vice versa. Instead of using access-denied ACEs, use access-allowed ACEs to explicitly allow the permitted access rights.

I couldn’t do a good job of translating the actual definition of ‘Synchronize’ earlier, but I think I can translate this paragraph. It’s saying that you can’t create an access denied ACE for just GENERIC_READ or just GENERIC_WRITE as they are defined, because each of those sets of rights include ‘Synchronize’, and you’d effectively be denying both sets of rights. GENERIC_READ (bit 31) and GENERIC_WRITE (bit 30) are two of the four “Generic access rights” mentioned above. When they’re translated/mapped to their object-specific rights, they make up a combination of bits 0-20 of the access mask (object specific and standard rights).

Once translated, GENERIC_READ is very similar to [FileSystemRights]::Read, and GENERIC_WRITE is very similar to [FileSystemRights]::Write. From the same MSDN page, here’s a list of the object specific and standard rights that make up the generic rights (the [FileSystemRights] equivalents are listed in parenthesis):

  • GENERIC_READ
    • FILE_READ_ATTRIBUTES (ReadAttributes)
    • FILE_READ_DATA (ReadData)
    • FILE_READ_EA (ReadExtendedAttributes)
    • STANDARD_RIGHTS_READ (ReadPermissions)
    • SYNCHRONIZE (Synchronize)
  • GENERIC_WRITE
    • FILE_APPEND_DATA (AppendData)
    • FILE_WRITE_ATTRIBUTES (WriteAttributes)
    • FILE_WRITE_DATA (WriteData)
    • FILE_WRITE_EA (WriteExtendedAttributes)
    • STANDARD_RIGHTS_WRITE (ReadPermissions)
    • SYNCHRONIZE (Synchronize)

The [FileSystemRights] enumeration has values for Read and Write that almost match what is defined above. Since PowerShell coerces strings into enumerations, and enumerations will attempt to show you combined flags where possible, let’s take a look at how those rights are seen when they’re cast as a FileSystemRights enumeration:

synchronize_4_generic_to_filesystemrights

Hopefully that makes sense. It’s showing that GENERIC_READ in [FileSystemRights] translates to ‘Read, Synchronize’, which means that GENERIC_READ is not the same as [FileSystemRights]::Read since ‘Read’ doesn’t include ‘Synchronize’. GENERIC_WRITE and [FileSystemRights]::Write are almost the same, except [FileSystemRights]::Write is also missing ‘ReadPermissions’ in addition to ‘Synchronize’.

So, why don’t the generic rights translate to the same numeric values for [FileSystemRights]? It goes back to the warning from the MSDN page above: if you want to deny ‘Read’ or ‘Write’ only, you have to remove the ‘Synchronize’ right first. The ACL editor does this, and it doesn’t give you any control over the ‘Synchronize’ right: if you create a new ACE it will determine whether or not the right is added, and it never shows it to you. The creators of the file/folder access control .NET classes didn’t get that luxury. Each ACE has a numeric access mask, and that access mask needs to be translated with a flags enumeration. If the ‘Synchronize’ bit is set, then the flags enumeration string is going to show it, and vice versa. So, they did the next best thing: they pulled ‘Synchronize’ from the combined ‘Read’ and ‘Write’ rights in the [FileSystemRights] enumeration, and made sure that creating a new allow ACE or audit rule automatically adds the ‘Synchronize’ right, and creating a new deny ACE removes it. If an application wants to hide the ‘Synchronize’ right from the end user, that’s fine, but the underlying .NET object will show it if it’s present.

I hope that makes sense and clears that up. If not, please leave a comment where something needs to be explained a little better, and I’ll try to expand on it some more.

Advertisements

UPDATE (Feb 8, 2014): If you need to audit or change the security descriptors for services on a regular basis, please check out this module instead of using the code in this post. It allows for quick auditing and/or modifying of security descriptors for files, folder, registry keys, printers, services, shares, processes, and more. If you find the module useful, please leave it a rating. If you have any questions, please use the Q&A section on the linked page.

In my last post, I showed an early version of a function to get the Discretionary Access Control List (DACL) of a Windows service. In this post, I’m going to show a newer version of that function, along with a function to change the DACL, and a helper function to create Access Control Entries (ACEs). The source code is quite a bit longer, so I’m not going to walk through each bit of code. What I will do is give a brief overview of each of the three functions, along with some examples of how to use them. I’ll also mention where I plan to take the functions in the future. I’ll include the source code of the functions as they currently stand at the end of the post. Included in the source code is comment based help for each of the three functions.

I recommend saving the source as a module and importing it, but you can also save it as a .ps1 file and dot-source it, or simply copy and paste it into an active session. I tried to make sure this works with PowerShell 2.0, but I can’t make any guarantees. If you find a problem with it, please let me know.

As always with these kinds of things, use these tools at your own risk. These functions work for what I need them to do, but I can’t guarantee that they will work for you and that there aren’t bugs in them.

As I said before, the code consists of three functions:

  1. Get-ServiceAcl
    The way this is currently set up, you can’t do any modifications without first getting an existing security descriptor. I didn’t include an easy mechanism to create one from scratch. The Get-ServiceAcl function adds a type name to the object that it returns that the Set-ServiceAcl function looks for. If that type doesn’t exist, Set-ServiceAcl won’t run. The Get-ServiceAcl function can get security descriptors for multiple services at a time. You can pass service names, service display names, or even service objects (returned from Get-Service) to this function. You can also pass a remote computer name (if you use Get-Service to get service objects, you can use the -ComputerName parameter in the call to that cmdlet).
  2. New-AccessControlEntry
    This function is used to create access control entries. I actually use it to create ACEs for files, folders, registry entries, and services. There are only two mandatory parameters: the access rights and the user/group principal. Use Get-Help to see explanations for all of the parameters, and for examples.
  3. Set-ServiceAcl
    This is the function that allows you to apply a modified security descriptor to a service. As I stated earlier, the function does a quick check to make sure the ACL you’re trying to apply originated from the Get-ServiceAcl function. The security descriptor can be set on the local machine, or on a remote machine. While I don’t recommend it unless you’ve fully tested whatever you’re trying to do, this function can be used to make changes to multiple services in a single call.

There are lots and lots of things you can do with these functions. There are examples of how to use each function in the help documentation, but I’ll cover a few sample uses below:

  • Here’s an example showing the basic get-modify-commit pattern to using all three functions. In it, we get the security descriptor of the WinRM service, add an entry to it, then save the changes.
    
    PS> $WinRmAcl = Get-ServiceAcl winrm
    PS> $WinRmAcl.Access
    
    PS> # Add an ACE allowing the 'INTERACTIVE' user Start and Stop service rights:
    PS> $WinRmAcl.AddAccessRule((New-AccessControlEntry -ServiceRights "Start,Stop" -Principal Interactive))
    
    PS> # Apply the modified ACL object to the service:
    PS> $WinRmAcl | Set-ServiceAcl
    
    PS> # Confirm the ACE was saved:
    PS> Get-ServiceAcl winrm | select -ExpandProperty Access
    
  • Here’s an example of listing the DACL contents in SDDL form for all of the device drivers on the local machine that start with the letter ‘b’:

    
    PS> [System.ServiceProcess.ServiceController]::GetDevices() | 
    >> where { $_.Name -like "b*" } | 
    >> Get-ServiceAcl | 
    >> fl ServiceName, SDDL
    >>
    
  • Here’s an example of listing the DACL contents for all services starting with the letter ‘b’ on a remote machine named ‘server01’, with the results grouped by user/group:

    
    PS> Get-Service -Name b* | 
    >> Get-ServiceAcl | 
    >> select -Property ServiceName -ExpandProperty Access | 
    >> sort IdentityReference | 
    >> fl -GroupBy IdentityReference -Property ServiceName, AccessControlType, ServiceRights
    >>
    

In the future, I want to expand the Get-ServiceAcl function to include an option for getting the system ACL (SACL) from a service or device driver. I originally thought that the sc.exe command wasn’t capable of getting the SACL, but it appears that it can if you’re running it as an administrator, so this should be a fairly simple modification. I’ll also have to modify the New-AccessControlEntry function to allow the creation of SACL entries.

I think that’s it for now. Hopefully these functions will be useful to someone else out there besides me 🙂

Here’s the source code:

<#
WARNING: These functions, especially Set-ServiceAcl, make it possible to make mass changes 
to the security descriptors of services, which can be dangerous from both a usability and 
security standpoint. DO NOT use these functions unless you have tested them in a controlled
environment. If you do use them, use them at your own risk. If you don't understand what
these functions do, please don't use them.

To use the functions, save the code as a script file, and dot source it. If you'd like to
hide the variables that are created in the global scope, you may save the file as a .psm1
module file, and use Import-Module to load it into the session. This should expose all
of the functions, but none of the variables.
#>

# [System.ServiceProcess.ServiceController] isn't loaded unless Get-Service has been
# called, so Add-Type just in case Get-Service hasn't been called yet
Add-Type -AssemblyName System.ServiceProcess

# There are several helper variables used that are loaded in global scope if script
# is dot-sourced. These are prefixed with __ to try to prevent collisions. Using the
# script as a module without exporting these variables makes this a much cleaner
# solution...


# Store namespace and enumeration name in variables so they can easily be changed
$__ServiceAclNamespace = "CustomNamespace.Services"
$__ServiceAccessFlagsEnumerationName = "ServiceAccessFlags"

# Store the full name for a faux object type that we'll store in our custom "service
# acl" objects:
$__ServiceAclTypeName = "ServiceAcl"
$__ServiceAclTypeFullName = "$__ServiceAclNamespace.$__ServiceAclTypeName"

# Add our service access mask enumeration:
Add-Type  @"
    namespace $__ServiceAclNamespace {
        [System.FlagsAttribute]
        public enum $__ServiceAccessFlagsEnumerationName : uint {
            QueryConfig = 1,
            ChangeConfig = 2,
            QueryStatus = 4,
            EnumerateDependents = 8,
            Start = 16,
            Stop = 32,
            PauseContinue = 64,
            Interrogate = 128,
            UserDefinedControl = 256,
            Delete = 65536,
            ReadControl = 131072,
            WriteDac = 262144,
            WriteOwner = 524288,
            Synchronize = 1048576,
            AccessSystemSecurity = 16777216,
            GenericAll = 268435456,
            GenericExecute = 536870912,
            GenericWrite = 1073741824,
            GenericRead = 2147483648
        }
    }
"@

$__ServiceAccessFlagsEnum = "$__ServiceAclNamespace.$__ServiceAccessFlagsEnumerationName" -as [type]
<#
.Synopsis
   Gets the security descriptor for a service or device driver.
.DESCRIPTION
   The Get-ServiceAcl function gets objects that represent the security descriptor of a 
   service or device driver. The security descriptor contains the access control lists 
   (ACLs) of the resources. The ACLs contain access control entries (ACEs) that specify 
   the permissions that users and/or groups have to access the resource.

   This function does not currently return the full security descriptor. It currently 
   only returns the Discretionary ACL (DACL), which contains access permissions. It does 
   not return the Owner, Group, or System ACL (which contains ACEs that control object 
   auditing). A future version will have the option to return the full security descriptor.
.PARAMETER ServiceName
   Specifies the name of a service. The function will get the security descriptor of the 
   service identified by the Name. Wildcards are permitted.
.PARAMETER DisplayName
   Specifies the display name of a service. The function will get the security descriptor 
   of the service identified by the DisplayName. Wildcards are permitted.
.PARAMETER ServiceObject
   Specifies an object of type [System.ServiceProcess.ServiceController]. The function 
   will get the security descriptor of the service identified by the object.
.PARAMETER ComputerName
   The name of the computer to get the service ACL from. The default is the local computer. 
   This parameter cannot be used with the -ServiceObject parameter since a ServiceController 
   object already has the computer name.
.EXAMPLE
   PS> Get-ServiceAcl -ServiceName WinRM
   
   This command gets the security descriptor for the WinRM service by specifying the name 
   of the service.
.EXAMPLE
   PS> Get-ServiceAcl -ServiceName WinRM -ComputerName server01
   
   This command gets the security descriptor for the WinRM service on a remote computer 
   named server01 by specifying the name of the service.
.EXAMPLE
   PS> Get-ServiceAcl -DisplayName "Windows Remote Management*"
   
   This command gets the security descriptor for the WinRM service by specifying the 
   display name of the service with a wildcard.
.EXAMPLE
   PS> Get-ServiceAcl -ServiceObject (Get-Service WinRM)

   This command gets the security descriptor for the WinRM service by passing a 
   ServiceController object obtained from Get-Service.
.EXAMPLE
   PS> Get-Service WinRM | Get-ServiceAcl

   This command gets the security descriptor for the WinRM service by passing a 
   ServiceController object obtained from Get-Service through the pipeline.
.EXAMPLE
   PS> "b*" | Get-ServiceAcl

   This command gets the security descriptors for all services that start with the 
   letter b on the local computer.
.EXAMPLE
   PS> Get-Service b* -ComputerName server01 | Get-ServiceAcl

   This command gets the security descriptors for all services that start with the 
   letter b on a remote computer named server01.
.EXAMPLE
   PS> [System.ServiceProcess.ServiceController]::GetDevices() | where { $_.Name -like "b*" } | Get-ServiceAcl

   This command gets the security descriptors for all device drivers that start with 
   the letter b on the local computer.
.EXAMPLE
   PS> [System.ServiceProcess.ServiceController]::GetDevices("server01") | where { $_.Name -like "b*" } | Get-ServiceAcl

   This command gets the security descriptors for all device drivers that start with 
   the letter b on a remote computer named server01.
.EXAMPLE
   PS> $Acl = Get-Service WinRM | Get-ServiceAcl
   PS> $Acl.AddAccessRule((New-AccessControlEntry -ServiceRights "Start,Stop" -Principal "Interactive"))
   PS> $Acl.SDDL
   PS> $Acl | Set-ServiceAcl

   This set of commands gets the security descriptor for the WinRM service on the local 
   machine and stores it to a variable named Acl. A new access control entry is then 
   added to the $Acl object, and the new SDDL is output to the screen. Finally, the 
   updated security descriptor is saved by using Set-ServiceAcl.
.NOTES
   The return object contains the following properties:
     - ServiceName: The name of the service where the security descriptor originated
     - ComputerName: The computer name where the service that contains the security 
       descriptor originated
     - SecurityDescriptor: The raw, untouched security descriptor (of type 
       System.Security.AccessControl.RawSecurityDescriptor)
     - Access: An array of ACEs in the Discretionary ACL
     - AccessToString: A string representation of the Access property
#>
function Get-ServiceAcl {
    [CmdletBinding(DefaultParameterSetName="ByName")]
    param(
        [Parameter(Mandatory=$true, Position=0, ValueFromPipeline=$true, ParameterSetName="ByName")]
        [Alias("Name")]
        [string[]] $ServiceName,
        [Parameter(Mandatory=$true, Position=0, ParameterSetName="ByDisplayName")]
        [string[]] $DisplayName,
        [Parameter(Mandatory=$true, Position=0, ValueFromPipeline=$true, ParameterSetName="ByServiceObject")]
        [System.ServiceProcess.ServiceController] $ServiceObject,
        [Parameter(Mandatory=$false, ParameterSetName="ByName")]
        [Parameter(Mandatory=$false, ParameterSetName="ByDisplayName")]
        [Alias('MachineName')]
        [string] $ComputerName = $env:COMPUTERNAME
    )

    begin {
        # Make sure enumeration has been added:
        if (-not ($__ServiceAccessFlagsEnum -is [type])) {
            Write-Warning "ServiceAccessFlags enumeration hasn't been loaded!"
            return
        }

        # Make sure computer has 'sc.exe':
        $ServiceControlCmd = Get-Command "$env:SystemRoot\system32\sc.exe"
        if (-not $ServiceControlCmd) {
            throw "Could not find $env:SystemRoot\system32\sc.exe command!"
        }
    }

    process {
        # Use Get-Service to our advantage to:
        #   1. Expand wild cards if user provided them to -ServiceName or -DisplayName parameters
        #   2. Ensure service(s) exist on local or remote computer
        switch ($PSCmdlet.ParameterSetName) {
            "ByName" {
                $Services = Get-Service -Name $ServiceName -ComputerName $ComputerName -ErrorAction Stop
            }

            "ByDisplayName" {
                $Services = Get-Service -DisplayName $DisplayName -ComputerName $ComputerName -ErrorAction Stop 
            }

            "ByServiceObject" {
                $Services = $ServiceObject
            }
        }

        # If function was called with 'ByName' or 'ByDisplayName' param sets, there may be 
        # multiple service objects, so step through each one:
        $Services | ForEach-Object {
        
            # We might need this info in catch block, so store it to a variable
            $CurrentServiceName = $_.Name
            $CurrentComputerName = $_.MachineName

            # Get SDDL using sc.exe
            $Sddl = & $ServiceControlCmd.Definition "\\$CurrentComputerName" sdshow "$CurrentServiceName" | Where-Object { $_ }

            try {
                # Get the DACL from the SDDL string
                $Dacl = New-Object System.Security.AccessControl.RawSecurityDescriptor($Sddl)
            }
            catch {
                Write-Warning "Couldn't get security descriptor for service '$CurrentName': $Sddl"
                return
            }

            # Create the custom object with the note properties
            $CustomObject = New-Object -TypeName PSObject -Property @{ ServiceName = $_.Name
                                                                       ComputerName = $_.MachineName
                                                                       SecurityDescriptor = $Dacl
                                                                     } | Select-Object ServiceName, ComputerName, SecurityDescriptor

            # Give the custom object a faux type name so that the Set-ServiceAcl function can easily tell
            # that the $AclObject passed is correct
            $CustomObject.PsObject.TypeNames.Insert(0, $__ServiceAclTypeFullName)

            # Add the 'Access' property:
            $CustomObject | Add-Member -MemberType ScriptProperty -Name Access -Value {
                $AccessRules = @($this.SecurityDescriptor.DiscretionaryAcl | ForEach-Object {
                    $CurrentDacl = $_

                    try {
                        $IdentityReference = $CurrentDacl.SecurityIdentifier.Translate([System.Security.Principal.NTAccount])
                    }
                    catch {
                        $IdentityReference = $CurrentDacl.SecurityIdentifier.Value
                    }
                
                    New-Object -TypeName PSObject -Property @{ ServiceRights = [System.Enum]::Parse($__ServiceAccessFlagsEnum, $CurrentDacl.AccessMask)
                                                               AccessControlType = $CurrentDacl.AceType
                                                               IdentityReference = $IdentityReference
                                                               IsInherited = $CurrentDacl.IsInherited
                                                               InheritanceFlags = $CurrentDacl.InheritanceFlags
                                                               PropagationFlags = $CurrentDacl.PropagationFlags
                                                             } | Select-Object ServiceRights, AccessControlType, IdentityReference, IsInherited, InheritanceFlags, PropagationFlags
                })

                # I had a lot of trouble forcing the return to be an array when there's only one object. I 
                # finally settled on using the unary comma to force an array to be returned (so, no, the 
                # comma isn't a typo).
                ,$AccessRules
                
            }

            # Add 'AccessToString' property that mimics a property of the same name from normal Get-Acl call
            $CustomObject | Add-Member -MemberType ScriptProperty -Name AccessToString -Value {
                $this.Access | ForEach-Object {
                    "{0} {1} {2}" -f $_.IdentityReference, $_.AccessControlType, $_.ServiceRights
                } | Out-String
            }

            # Add 'RemoveAccessRule' method to mimic the method of the same name from a normal Get-Acl call
            $CustomObject | Add-Member -MemberType ScriptMethod -Name RemoveAccessRule -Value {
                param(
                    [int] $Index = (Read-Host "Enter the index of the Access Control Entry")
                )

                $this.SecurityDescriptor.DiscretionaryAcl.RemoveAce($Index)
            }

            # Add 'AddAccessRule' method
            $CustomObject | Add-Member -MemberType ScriptMethod -Name AddAccessRule -Value {

                param(
                    [System.Security.AccessControl.CommonAce] $Rule
                )

                if (-not $Rule) {
                    Write-Warning "You  must provide an object of type System.Security.AccessControl.CommonAce to this method!"
                    return
                }

                $this.SecurityDescriptor.DiscretionaryAcl.InsertAce($this.SecurityDescriptor.DiscretionaryAcl.Count, $Rule)

            }

            # Add 'Sddl' property that returns the SDDL of the Acl object
            $CustomObject | Add-Member -MemberType ScriptProperty -Name Sddl -Value {
                $this.SecurityDescriptor.GetSddlForm("All")
            }

            # Emit the custom return object
            $CustomObject
        }
    }
}

<#
.Synopsis
   Changes the security descriptor of a service or device driver.
.DESCRIPTION
   The Set-ServiceAcl function changes the security descriptor of a service or device driver to match the 
   values in a security descriptor that you supply.

   NOTE: This function makes it possible to make mass changes to the security descriptors on services, which 
   can be dangerous from both a usability and security standpoint. Please do not use this function unless 
   you have tested it fully, and you know exactly what those changes will do.
.PARAMETER AclObject
   Specifies a security descriptor with the desired property values. Set-ServiceAcl changes the security 
   descriptor of the service specified by the ServiceName, DisplayName, or ServiceController object to match 
   the values in the specified AclObject
.PARAMETER ServiceName
   Specifies the name of a service. The function will change the security descriptor of the service 
   identified by the Name. Wildcards are permitted.
.PARAMETER DisplayName
   Specifies the display name of a service. The function will change the security descriptor of the service 
   identified by the DisplayName. Wildcards are permitted.
.PARAMETER ServiceObject
   Specifies an object of type [System.ServiceProcess.ServiceController]. The function will change the 
   security descriptor of the service identified by the object.
.PARAMETER ComputerName
   The name of the computer that contains the object that will have its security descriptor changed. The 
   default is the local computer. This parameter cannot be used with the -ServiceObject parameter since a 
   ServiceController object already has the computer name.
.EXAMPLE
   PS> $Acl = Get-Service WinRM | Get-ServiceAcl
   PS> $Acl.AddAccessRule((New-AccessControlEntry -ServiceRights "Start,Stop" -Principal "Interactive"))
   PS> $Acl | Set-ServiceAcl

   This set of commands gets the security descriptor for the WinRM service on the local machine and stores it 
   to a variable named Acl. A new access control entry is then added to the $Acl object that gives the 
   interactive user Start and Stop rights over the service, and the updated security descriptor is saved by 
   using Set-ServiceAcl. 
   
   Because the $Acl object contains the service name and computer name, they did not have to be explicitly 
   specified to the function.
.EXAMPLE
   PS> $Acl = Get-Service WinRM | Get-ServiceAcl
   PS> $Acl.AddAccessRule((New-AccessControlEntry -ServiceRights "Start,Stop" -Principal "Interactive"))
   PS> $Acl | Set-ServiceAcl -ServiceName bits, wi* -WhatIf

   This set of commands gets the security descriptor for the WinRM service on the local machine and stores it to 
   a variable named Acl. A new access control entry is then added to the $Acl object that gives the interactive 
   user Start and Stop rights over the service. The security descriptor is then saved to the bits service, and 
   all services that have a service name that starts with 'wi'.  (NOTE: The -WhatIf parameter stops the security 
   descriptor from actually being changed for the services in question. Doing a mass ACL change on multiple 
   services this way is not recommended.)
.EXAMPLE
   PS> $Acl = Get-Service WinRM -ComputerName server01 | Get-ServiceAcl
   PS> $Acl.AddAccessRule((New-AccessControlEntry -ServiceRights "Start,Stop" -Principal "Interactive"))
   PS> $Acl | Set-ServiceAcl -ServiceName bits, wi* -WhatIf

   This set of commands gets the security descriptor for the WinRM service on the remote computer 'server01' and 
   stores it to a variable named Acl. A new access control entry is then added to the $Acl object that gives the 
   interactive user Start and Stop rights over the service. The security descriptor is then saved to the bits 
   service on the remote computer, and all services that have a service name that starts with 'wi' on the remote 
   computer.  (NOTE: The -WhatIf parameter stops the security descriptor from actually being changed for the 
   services in question. Doing a mass ACL change on multiple services this way is not recommended.)

   Because the ACL object contains the remote computer name, any call to Set-ServiceAcl without explicitly 
   providing the -ComputerName parameter to the function will modify the security descriptor on the remote machine.
#>
function Set-ServiceAcl {
	[CmdletBinding(DefaultParameterSetName="ByName",SupportsShouldProcess=$true)]
	param(
        [Parameter(Mandatory=$true, Position=0, ValueFromPipelineByPropertyName=$true, ParameterSetName="ByName")]
        [Alias("Name")]
        [string[]] $ServiceName,
        [Parameter(Mandatory=$true, Position=0, ParameterSetName="ByDisplayName")]
        [string[]] $DisplayName,
        [Parameter(Mandatory=$true, Position=0, ParameterSetName="ByServiceObject")]
        [System.ServiceProcess.ServiceController] $ServiceObject,
        [Parameter(Mandatory=$true, ValueFromPipeline=$true)]
        #[ValidateScript({ $this.PsObject.TypeNames -contains $__ServiceAclTypeFullName })]
        $AclObject,
        [Parameter(Mandatory=$false, ValueFromPipelineByPropertyName=$true, ParameterSetName="ByName")]
        [Parameter(Mandatory=$false, ParameterSetName="ByDisplayName")]
        [string] $ComputerName = $env:COMPUTERNAME
	)									

	begin {
        # Make sure computer has 'sc.exe':
        $ServiceControlCmd = Get-Command "$env:SystemRoot\system32\sc.exe"
        if (-not $ServiceControlCmd) {
            throw "Could not find $env:SystemRoot\system32\sc.exe command!"
        }
	}
	
	process {

		# Validate AclObject:
        if (-not ($AclObject.PsObject.TypeNames -contains $__ServiceAclTypeFullName)) {
            Write-Warning "`$AclObject is not a valid Service Acl object (`$AclObject must be created by calling Get-ServiceAcl)"
            return
        }

        # Use Get-Service to our advantage to:
        #   1. Expand wild cards if user provided them to -ServiceName or -DisplayName parameters
        #   2. Ensure service(s) exist on local or remote computer
        switch ($PSCmdlet.ParameterSetName) {
            "ByName" {
                $Services = Get-Service -Name $ServiceName -ComputerName $ComputerName -ErrorAction Stop
            }

            "ByDisplayName" {
                $Services = Get-Service -DisplayName $DisplayName -ComputerName $ComputerName -ErrorAction Stop 
            }

            "ByServiceObject" {
                $Services = $ServiceObject
            }
        }

        # If function was called with 'ByName' or 'ByDisplayName' param sets, there may be multiple service objects,
        # so step through each one:
        $Services | ForEach-Object {

            # Get SDDL:
            $Sddl = $AclObject.Sddl

            $CurrentComputerName = $_.MachineName
            $CurrentServiceName = $_.Name

            $ShouldProcessDescription = "Set ACL for service '{0}' on computer '{1}' to $Sddl" -f $CurrentServiceName, $CurrentComputerName
            if ($PSCmdlet.ShouldProcess("$ShouldProcessDescription`.", "$ShouldProcessDescription`?", "Confirm ACL Change")) {
                $Arguments = '"\\{0}" sdset "{1}" "{2}"' -f $CurrentComputerName, $CurrentServiceName, $Sddl

                Write-Verbose ("Running command: {0} {1}" -f $ServiceControlCmd.Definition, $Arguments)
                $ReturnString = & $ServiceControlCmd.Definition $Arguments
                Write-Verbose "Output from sc.exe: $ReturnString"

                # Not sure if the return string is the same for all OSes, hoping this basic regex will work
                if ($ReturnString -notmatch "SUCCESS$") {
                    Write-Warning "Error setting ACL: $ReturnString"
                }
            }
        }            
	}
}

<#
.Synopsis
   Creates a new access control entry for a securable object.
.DESCRIPTION
   The New-AccessControlEntry function creates access control entries (ACEs) that can be added to access control 
   lists (ACLs).

   The function currently supports creating ACEs for registry rights, file and folder rights, and service rights.
.EXAMPLE
   PS> New-AccessControlEntry -FileSystemRights Modify -Principal Users -InheritanceFlags ContainerInherit,ObjectInherit

   This command creates an ACE allowing file modify rights to the 'Users' local group, with ContainerInherit and 
   ObjectInherit inheritance flags.
.EXAMPLE
   PS> New-AccessControlEntry -RegistryRights FullControl -Principal Users

   This command creates an ACE allowing registry full control rights to the 'Users' local group.
.EXAMPLE
   PS> New-AccessControlEntry -ServiceRights "Start,Stop" -Principal Users

   This command creates an ACE allowing service Start and Stop rights to the 'Users' local group.
.PARAMETER FileSystemRights
   Specifies the file system rights for the ACE. This parameter cannot be used with the RegistryRights or 
   ServiceRights parameters.

   Valid values can be found in the [System.Security.AccessControl.FileSystemRights] enumeration.
.PARAMETER RegistryRights
   Specifies the registry rights for the ACE. This parameter cannot be used with the FileSystemRights or 
   ServiceRights parameters.

   Valid values can be found in the [System.Security.AccessControl.RegistryRights] enumeration.
.PARAMETER ServiceRights
   Specifies the service rights for the ACE. This parameter cannot be used with the FileSystemRights or 
   RegistryRights parameters.

   Valid values can be found in a custom enumeration defined at the same time as this function.
.PARAMETER Principal
   Specifies a user or group account.
.PARAMETER InheritanceFlags
   Specifies any inheritance flags for the ACE.
.PARAMETER PropagationFlags
   Specifies any propagation flags for the ACE.
.PARAMETER AccessControlType
   Specifies whether or not the ACE is an Allow or Deny entry.
.NOTES
   The function currently only creates ACEs that can be used with discretionary ACLs, but it can easily be 
   extended to work with system ACLs (used for auditing). The functionc an also be easily extended to work 
   with any other securable object, e.g., printers, shares, etc.
#>
function New-AccessControlEntry {
    [CmdletBinding(DefaultParameterSetName='File')]
    param(
        [Parameter(Mandatory=$true, ParameterSetName='File')]
        [System.Security.AccessControl.FileSystemRights] $FileSystemRights,
        [Parameter(Mandatory=$true, ParameterSetName='Registry')]
        [System.Security.AccessControl.RegistryRights] $RegistryRights,
        [Parameter(Mandatory=$true, ParameterSetName='Service')]
        [string] $ServiceRights,
        [Parameter(Mandatory=$true)]
        [Alias('User','Group','IdentityReference')]
        [System.Security.Principal.NTAccount] $Principal,
        [System.Security.AccessControl.InheritanceFlags] $InheritanceFlags = "None",
        [System.Security.AccessControl.PropagationFlags] $PropagationFlags = "None",
        [System.Security.AccessControl.AccessControlType] $AccessControlType = "Allow"
    )


    switch ($PSCmdlet.ParameterSetName) {

        "File" {
            $AccessControlObject = "System.Security.AccessControl.FileSystemAccessRule"

            $Arguments = @( $Principal         # System.String
                            $FileSystemRights  # System.Security.AccessControl.FileSystemRights
                            $InheritanceFlags  # System.Security.AccessControl.InheritanceFlags
                            $PropagationFlags  # System.Security.AccessControl.PropagationFlags
                            $AccessControlType # System.Security.AccessControl.AccessControlType
                          )
            break
        }

        "Registry" {
            $AccessControlObject = "System.Security.AccessControl.RegistryAccessRule"

            $Arguments = @( $Principal         # System.String
                            $RegistryRights    # System.Security.AccessControl.RegistryRights
                            $InheritanceFlags  # System.Security.AccessControl.InheritanceFlags
                            $PropagationFlags  # System.Security.AccessControl.PropagationFlags
                            $AccessControlType # System.Security.AccessControl.AccessControlType
                          )
            break
        }

        "Service" {
            
            if (-not ($__ServiceAccessFlagsEnum -is [type])) {
                Write-Warning "[ServiceAccessFlags] enumeration has not been added; cannot create ACE"
                return
            }

            $AccessControlObject = "System.Security.AccessControl.CommonAce"

            # AceQualifer has four possible values: AccessAllowed, AccessDenied, SystemAlarm, and SystemAudit
            # We need to convert AccessControlType (can only be Allow or Deny) to proper AceQualifer enum
            if ($AccessControlType -eq "Allow") { $AceQualifer = "AccessAllowed" }
            else { $AceQualifer = "AccessDenied" }

            # Make sure $ServiceRights are valid:
            try {
                $AccessMask = [int] [System.Enum]::Parse($__ServiceAccessFlagsEnum, $ServiceRights, $true)  # Third argument means ignore case
            }
            catch {
                Write-Warning "Invalid ServiceRights defined; cannot create ACE"
                return
            }

            $Arguments = @( "None"       # System.Security.AccessControl.AceFlags
                            $AceQualifer # System.Security.AccessControl.AceQualifier
                            $AccessMask
                            $Principal.Translate([System.Security.Principal.SecurityIdentifier])
                            $false       # isCallback?
                            $null        # opaque data (only for callbacks)
                          )
            break
        }

        default {
            Write-Warning "Unknown ParameterSetName" 
            return
        }

    }

    # Create the ACE object
    New-Object -TypeName $AccessControlObject -ArgumentList $Arguments
}

UPDATE (Feb 8, 2014): If you need to audit or change the security descriptors for services on a regular basis, please check out this module instead of using the code in this post. It allows for quick auditing and/or modifying of security descriptors for files, folder, registry keys, printers, services, shares, processes, and more. If you find the module useful, please leave it a rating. If you have any questions, please use the Q&A section on the linked page.

PowerShell makes viewing and editing ACLs on files and registry paths easy (well, at least doable) via the Get-Acl and Set-Acl cmdlets. To my knowledge, though, there isn’t a way to view or change service ACLs with these cmdlets. Here’s how I deal with viewing the ACLs. I’ll have a separate post on how to change the ACLs soon.

First, we need a way to represent the ACL. It turns out that there is a generic .NET class that can represent any security descriptor: System.Security.AccessControl.RawSecurityDescriptor. Let’s look at how to create a new instance of this class:


PS > [System.Security.AccessControl.RawSecurityDescriptor].GetConstructors() |
>> foreach ToString
>>
Void .ctor(System.Security.AccessControl.ControlFlags, System.Security.Principal.
SecurityIdentifier, System.Security.Principal.SecurityIdentifier, System.Security.
AccessControl.RawAcl, System.Security.AccessControl.RawAcl)
Void .ctor(System.String)
Void .ctor(Byte[], Int32)

There are three constructors. The last two are the most interesting for this post, though. One takes a single string: the string representation of the Security Descriptor Definition Language (SDDL) for the ACL. The other one takes a byte array and an integer: the binary form of the ACL and an offset specifying where to being reading from the byte array.

I know of three ways to get the ACL information for a service object (there are probably more, though):

  1. Using sc.exe with the sdshow argument:
    
    PS > $Sddl = sc.exe sdshow wuauserv | where { $_ }
    PS > New-Object System.Security.AccessControl.RawSecurityDescriptor ($Sddl)
    
    ControlFlags           : DiscretionaryAclPresent, SelfRelative
    Owner                  : 
    Group                  : 
    SystemAcl              : 
    DiscretionaryAcl       : {System.Security.AccessControl.CommonAce, 
                             System.Security.AccessControl.CommonAce, 
                             System.Security.AccessControl.CommonAce}
    ResourceManagerControl : 0
    BinaryLength           : 92
    

    The biggest downside with this method is that only the DACL is being returned. We aren’t getting the Owner, Group or SACL information. As far as I can tell, the sc.exe program will only return DACL information.

  2. Reading from the registry:
    
    PS > $RegPath = "HKLM:\SYSTEM\CurrentControlSet\Services\wuauserv\Security"
    PS > $BinarySddl = Get-ItemProperty $RegPath | select -Expand Security
    PS > New-Object System.Security.AccessControl.RawSecurityDescriptor ($BinarySddl, 0)
    
    ControlFlags           : DiscretionaryAclPresent, SystemAclPresent, SelfRelative
    Owner                  : S-1-5-18
    Group                  : S-1-5-18
    SystemAcl              : {System.Security.AccessControl.CommonAce}
    DiscretionaryAcl       : {System.Security.AccessControl.CommonAce, 
                             System.Security.AccessControl.CommonAce, 
                             System.Security.AccessControl.CommonAce}
    ResourceManagerControl : 0
    BinaryLength           : 144
    </code

    With this method, we're getting more information than was available using sc.exe. The biggest downside with this method is that not all services appear to have this information listed in the registry.

  3. Using the QueryServiceObjectSecurity function from 'advapi32.dll':
    This would require platform invoking, and it's much more complicated (and takes a lot more code) than the previous two methods. We're not going to cover this method in this post, but I will likely make a post covering how to do this at some point in the future. (Please leave a comment if you're interested in it)

For the rest of the post, we're going to use the sc.exe method. It only contains the DACL information, but we're not going to worry about the Owner, Group, or SACL for this post. If you're interested in viewing and/or modifying those, feel free to modify the part of the final function that creates the RawSecurityDescriptor object.

Once we have our RawSecurityDescriptor object, we can actually view the DACL (remember, a Discretionary Access Control List is just a collection of Access Control Entries, or ACEs):


PS > # Store the RawSecurityDescriptor in $sd variable:
PS > $Sddl = sc.exe sdshow wuauserv | where { $_ }
PS > $sd = New-Object System.Security.AccessControl.RawSecurityDescriptor ($Sddl)
PS > $sd.DiscretionaryAcl.GetType()
IsPublic IsSerial Name       BaseType
-------- -------- ----       --------
True     False    RawAcl     System.Security.AccessControl.GenericAcl

PS > $sd.DiscretionaryAcl[0].GetType()
IsPublic IsSerial Name       BaseType
-------- -------- ----       --------
True     False    CommonAce  System.Security.AccessControl.QualifiedAce

PS > # ACL contains 3 ACEs
PS > $sd.DiscretionaryAcl.Count
3

PS > # List the first ACE in DACL:
PS > $sd.DiscretionaryAcl[0]
BinaryLength       : 20
AceQualifier       : AccessAllowed
IsCallback         : False
OpaqueLength       : 0
AccessMask         : 131229
SecurityIdentifier : S-1-5-11
AceType            : AccessAllowed
AceFlags           : None
IsInherited        : False
InheritanceFlags   : None
PropagationFlags   : None
AuditFlags         : None

We've got access to the ACEs, but they're not very readable. We can tell that this ACE is allowing access, but that's about it. The AccessMask tells us what kind of access is being allowed, but we (probably) have no idea what 131229 means. The SecurityIdentifier tells us who is getting this access, but we (probably) don't know to what user or group it is referring. When you get file or registry ACLs, they have a nice 'Access' codeproperty that gives you a much friendlier representation of the ACE. Let's see if we can't make one for our service ACEs.

First, we need to figure out how to convert the numeric access mask into something that is readable. An enumeration would be perfect for this since it allows you to convert both ways: numbers to text, and text to numbers. It took a little searching, but I was eventually able to find enough information to build a flags enumeration. I'm not really going to go into what that means, but I will be more than happy to dedicate a post on enumerations if there is enough interest. Here's the code to add the enumeration to our session:


PS > Add-Type  @"
  [System.FlagsAttribute]
  public enum ServiceAccessFlags : uint
  {
      QueryConfig = 1,
      ChangeConfig = 2,
      QueryStatus = 4,
      EnumerateDependents = 8,
      Start = 16,
      Stop = 32,
      PauseContinue = 64,
      Interrogate = 128,
      UserDefinedControl = 256,
      Delete = 65536,
      ReadControl = 131072,
      WriteDac = 262144,
      WriteOwner = 524288,
      Synchronize = 1048576,
      AccessSystemSecurity = 16777216,
      GenericAll = 268435456,
      GenericExecute = 536870912,
      GenericWrite = 1073741824,
      GenericRead = 2147483648
  }
"@

PS > # Find out what access mask of 131229 in previous example means:
PS > [ServiceAccessFlags] 131229
QueryConfig, QueryStatus, EnumerateDependents, Start, Interrogate, ReadControl


Now we're getting somewhere! Now we need to figure out how to translate the SID into a user or group. Thankfully, there is a built-in method on the SecurityIdentifier object that allows us to translate:


PS > $sd.DiscretionaryAcl[0].SecurityIdentifier.Translate.OverloadDefinitions
System.Security.Principal.IdentityReference Translate(type targetType)

PS > $sd.DiscretionaryAcl[0].SecurityIdentifier.Translate(
>> [System.Security.Principal.NTAccount])
>>

Value
-----
NT AUTHORITY\Authenticated Users

So, we can see the ACEs in the DACL, and we can get readable versions of the user/group and the access permissions. Now, let's use what we know to create a function that takes a service name and returns a custom object that contains the service name, the DACL, and a custom ScriptProperty called Access that mimics the Access property returned from Get-Acl and has the ACEs in a more readable format (PowerShell v2 users should remove the two [ordered] type accelerator instances to get this to work):

Add-Type  @"
  [System.FlagsAttribute]
  public enum ServiceAccessFlags : uint
  {
      QueryConfig = 1,
      ChangeConfig = 2,
      QueryStatus = 4,
      EnumerateDependents = 8,
      Start = 16,
      Stop = 32,
      PauseContinue = 64,
      Interrogate = 128,
      UserDefinedControl = 256,
      Delete = 65536,
      ReadControl = 131072,
      WriteDac = 262144,
      WriteOwner = 524288,
      Synchronize = 1048576,
      AccessSystemSecurity = 16777216,
      GenericAll = 268435456,
      GenericExecute = 536870912,
      GenericWrite = 1073741824,
      GenericRead = 2147483648
  }
"@

function Get-ServiceAcl {
    [CmdletBinding(DefaultParameterSetName="ByName")]
    param(
        [Parameter(Mandatory=$true, Position=0, ValueFromPipeline=$true, ParameterSetName="ByName")]
        [string[]] $Name,
        [Parameter(Mandatory=$true, Position=0, ParameterSetName="ByDisplayName")]
        [string[]] $DisplayName,
        [Parameter(Mandatory=$false, Position=1)]
        [string] $ComputerName = $env:COMPUTERNAME
    )

    # If display name was provided, get the actual service name:
    switch ($PSCmdlet.ParameterSetName) {
        "ByDisplayName" {
            $Name = Get-Service -DisplayName $DisplayName -ComputerName $ComputerName -ErrorAction Stop | 
                Select-Object -ExpandProperty Name
        }
    }

    # Make sure computer has 'sc.exe':
    $ServiceControlCmd = Get-Command "$env:SystemRoot\system32\sc.exe"
    if (-not $ServiceControlCmd) {
        throw "Could not find $env:SystemRoot\system32\sc.exe command!"
    }

    # Get-Service does the work looking up the service the user requested:
    Get-Service -Name $Name | ForEach-Object {
        
        # We might need this info in catch block, so store it to a variable
        $CurrentName = $_.Name

        # Get SDDL using sc.exe
        $Sddl = & $ServiceControlCmd.Definition "\\$ComputerName" sdshow "$CurrentName" | Where-Object { $_ }

        try {
            # Get the DACL from the SDDL string
            $Dacl = New-Object System.Security.AccessControl.RawSecurityDescriptor($Sddl)
        }
        catch {
            Write-Warning "Couldn't get security descriptor for service '$CurrentName': $Sddl"
            return
        }

        # Create the custom object with the note properties
        $CustomObject = New-Object -TypeName PSObject -Property ([ordered] @{ Name = $_.Name
                                                                              Dacl = $Dacl
                                                                            })

        # Add the 'Access' property:
        $CustomObject | Add-Member -MemberType ScriptProperty -Name Access -Value {
            $this.Dacl.DiscretionaryAcl | ForEach-Object {
                $CurrentDacl = $_

                try {
                    $IdentityReference = $CurrentDacl.SecurityIdentifier.Translate([System.Security.Principal.NTAccount])
                }
                catch {
                    $IdentityReference = $CurrentDacl.SecurityIdentifier.Value
                }
                
                New-Object -TypeName PSObject -Property ([ordered] @{ 
                                ServiceRights = [ServiceAccessFlags] $CurrentDacl.AccessMask
                                AccessControlType = $CurrentDacl.AceType
                                IdentityReference = $IdentityReference
                                IsInherited = $CurrentDacl.IsInherited
                                InheritanceFlags = $CurrentDacl.InheritanceFlags
                                PropagationFlags = $CurrentDacl.PropagationFlags
                                                                    })
            }
        }

        # Add 'AccessToString' property that mimics a property of the same name from normal Get-Acl call
        $CustomObject | Add-Member -MemberType ScriptProperty -Name AccessToString -Value {
            $this.Access | ForEach-Object {
                "{0} {1} {2}" -f $_.IdentityReference, $_.AccessControlType, $_.ServiceRights
            } | Out-String
        }

        $CustomObject
    }
}

Here's a quick example of using the previous code:


PS > "wuauserv" | Get-ServiceAcl | select -ExpandProperty Access

ServiceRights     : QueryConfig, QueryStatus, EnumerateDependents, Start, 
                    Interrogate, ReadControl
AccessControlType : AccessAllowed
IdentityReference : NT AUTHORITY\Authenticated Users
IsInherited       : False
InheritanceFlags  : None
PropagationFlags  : None

ServiceRights     : QueryConfig, ChangeConfig, QueryStatus, 
                    EnumerateDependents, Start, Stop, PauseContinue, 
                    Interrogate, UserDefinedControl, Delete, ReadControl, 
                    WriteDac, WriteOwner
AccessControlType : AccessAllowed
IdentityReference : BUILTIN\Administrators
IsInherited       : False
InheritanceFlags  : None
PropagationFlags  : None

ServiceRights     : QueryConfig, ChangeConfig, QueryStatus, 
                    EnumerateDependents, Start, Stop, PauseContinue, 
                    Interrogate, UserDefinedControl, Delete, ReadControl, 
                    WriteDac, WriteOwner
AccessControlType : AccessAllowed
IdentityReference : NT AUTHORITY\SYSTEM
IsInherited       : False
InheritanceFlags  : None
PropagationFlags  : None

There's still a lot that can be done with this. I plan to have a post up soon showing how to easily change the ACLs on services as well. One day I'd also like to convert this into a Proxy function for Get-Acl so that the new function can get ACLs for files, folders, registry keys, and services.