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):
- 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.
- 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.
- 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.