Viewing Service ACLs

Posted: March 19, 2013 in PowerShell
Tags: , , ,

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.

Comments
  1. […] Viewing Service ACLs […]

  2. Roger says:

    Did you Post a sample using the QueryServiceObjectSecurity function from ‘advapi32.dll’?

  3. Rohn Edwards says:

    I didn’t. Version 3 of the module that I linked to in the note at the beginning uses the GetNamedSecurityInfo() function instead. It’s a script module, so you can see the code. Version 4 uses the same function, but it uses C# code. Did you still want to see how to use QueryServiceObjectSecurity() from PowerShell? I don’t mind doing a quick post (or maybe just linking to a GitHub gist).

    • Roger says:

      Hi Rohn, Thanks for getting back to me, yes I’m really just interested in seeing how to use QueryServiceObjectSecurity() from PowerShell. If you have the time then I’n really seen to see how you would do it.

  4. Rohn Edwards says:

    I’d start with something like this: https://gist.github.com/rohnedwards/ff18e60d41cd1ca44d25

    It would still need a lot of work before I’d consider it done. Have you checked out the PowerShell Access Control module version 4.0? I think it handles service access control really well…

    • Roger says:

      Thanks for all your help Rohn. The main reason I wanted to see how you did it was because I had worked on some of the code and got struck in a few places (as I’m new to doing this). Your sample has helped me resolve the problems I was having and helped me better understand how to correctly call some of the code.

      • Rohn Edwards says:

        You’re very welcome. I’m glad you were able to get it working. One thing I’ll point out is that I used a SafeHandle in the P/Invoke signature b/c I noticed there was one available with the ServiceController class. SafeHandles are awesome when they’re available because you don’t have to worry about obtaining and closing the handle. If there wasn’t one, though, I would have used an IntPtr in the signature, and then I would have had to worry about getting the handle myself (I did notice a private ServiceController method that looked like it would get the handle, but I didn’t test it out…otherwise there would be more P/Invoke signatures for Win32 calls).

      • Roger says:

        Thanks for the extra information about safehandle, very helpful.

  5. […] Viewing Service ACLs Das Abfragen der ACL eines Systemdienstes per PowerShell ist grundsätzlich zwar möglich, aber etwas aufwändig. […]

Leave a reply to Roger Cancel reply