There’s a new version of my PowerShellAccessControl module available in the Script Center Repository. It’s got a lot of new stuff in it, so go check it out. I’m going to just show you some random commands to run (and I’ll post a screenshot or two).

The update brings the ability to audit/modify SACLs (object auditing). It also simplifies getting security descriptors for several objects, including services, printers, WMI namespaces, and more (even file/folder and registry entries). PowerShell v2 is also supported (let me know if you find anything that doesn’t work when using v2). It’s still lacking the functions I’ve been working on to actually save the security descriptors back out. Don’t worry, though, because those are coming. I’m also working on other ways for the module to get its hands on more security descriptors.

For now, though, I think it does a pretty good job of letting you audit your ACLs for almost anything you’d want to (and if it doesn’t directly support the object, you can still use New-AdaptedSecurityDescriptor if you know the SDDL or binary form of the SD). You can also use New-AccessControlEntry to create file, folder, and registry ACEs to use with Get-Acl and Set-Acl. That by itself saves several lines of code.

Anyway, go download the module, then run through some of these demo scripts line by line:

Working with services:

# Must be run as admin b/c of GetSecurityDescriptor WMI method

# Get BITs service SD
$SD = Get-Service bits | Get-SecurityDescriptor

# Check out the default formatting
$SD

# And as a list:
$SD | fl

# Show ACEs (access and audit) for services that start with the letter 'B'
Get-Service b* | Get-AccessControlEntry

# Show auditing ACEs for services that start with the letter 'B'
Get-Service b* | Get-AccessControlEntry -AclType Audit

# Get BITs service SD (again)
$SD = Get-Service bits | Get-SecurityDescriptor

# Give users the ability to Start and Stop it:
$SD.AddAccessRule((New-AccessControlEntry -ServiceAccessRights Start, Stop -Principal Users))
# Audit that
$SD.AddAuditRule((New-AccessControlEntry -ServiceAccessRights Start, Stop -Principal Users -AuditSuccess))

# Look to make sure those entries are there:
$SD | fl
#or
$SD.Access
$SD.Audit

# Since there's no Set-SecurityDescriptor yet, do this if you want to save
# SD (you have to remove -WhatIf to make it permanent)
$Win32SD = $SD | ConvertTo-Win32SecurityDescriptor -ValueOnly  # Use -LegacyWmiObject if you're going to use WMI cmdlets
Get-CimInstance Win32_Service -Filter "Name='bits'" | Invoke-CimMethod -MethodName SetSecurityDescriptor -WhatIf -Arguments @{
    Descriptor = $Win32SD
}

Working with other objects:

# Printers
Get-WmiObject Win32_Printer | Get-SecurityDescriptor

# Printer access ACE:
$ACE = New-AccessControlEntry -PrinterRights ManageDocuments -Principal Users

# Logical share:
Get-CimInstance Win32_LogicalShareSecuritySetting | Get-SecurityDescriptor

# WSMan:
dir wsman: -Recurse | ? { $_.Name -eq "SDDL" } | Get-SecurityDescriptor

# Folder:
get-item c:\windows | Get-SecurityDescriptor
dir c:\windows -Directory | Get-AccessControlEntry -AceNotInherited
dir c:\windows -Directory | Get-AccessControlEntry -IdentityReference Administrators

# WMI namespace:
$SD = Get-CimInstance __SystemSecurity -Namespace root/cimv2 | Get-SecurityDescriptor

# Add an access ACE that also applies to all child namespaces:
$SD.AddAccessRule((New-AccessControlEntry -WmiNamespaceRights RemoteEnable -Principal Users -AppliesTo Object, ChildContainers))

# View the modified ACL:
$SD.Access
 
# Get new SDDL:
$SD.Sddl
 
# Get new binary form:
$SD.GetSecurityDescriptorBinaryForm()

# Remember, WMI namespace SD hasn't been modified for real, just in in-memory instance of SD

Audit all WMI namespace rights:

function Get-ChildNamespace {

    param(
        [string] $Namespace = "root",
        [int] $Level = 1
    )

    # Decrement level (if argument wasn't supplied, you'll only get
    # the direct chidren)
    $Level--

    Get-WmiObject __Namespace -Namespace $Namespace | select -exp name | ForEach-Object {

        [PsCustomObject] @{
            FullName = "$Namespace\$_"
            Name = $_
            Namespace = $Namespace
        }

        # Negative numbers mean recurse forever
        if ($Level) {
            & $MyInvocation.MyCommand -Namespace "$Namespace\$_" -RecurseLevel $Level
        }
    }
}

# Store SDs for all namesapces in $WmiNsSD (gwmi)
$WmiSDs = Get-ChildNamespace -Level -1 | 
    select -exp fullname | 
    % {"root"}{ $_ } | 
    sort | 
    % { Get-WmiObject -EnableAllPrivileges __SystemSecurity -Namespace $_ } | 
    Get-SecurityDescriptor

# Just show with default formatting:
$WmiSDs

# Or show with ACEs expanded as their own objects
$WmiSDs | Get-AccessControlEntry


# Only show ACEs that aren't inherited:
$WmiSDs | Get-AccessControlEntry -AceNotInherited

And here is a screenshot showing the default formatting after calling ‘Get-Service b* | Get-AccessControlEntry’:

Get-Service b* | Get-AccessControlEntry

Get-Service b* | Get-AccessControlEntry

And one after calling ‘Get-Service bits | Get-SecurityDescriptor’

Get-Service | Get-SecurityDescriptor

Get-Service | Get-SecurityDescriptor

There are a lot of examples in the comment based help for the functions, too. If you have any issues/suggestions, please let me know.

I saw a post yesterday on the Scripting Guys forum that pointed me to a function that I absolutely fell in love with. I ended up spending a few hours messing around, and I came up with something I hope met the original poster’s requirements.

The poster was looking for a way to find out if ‘Strict Mode’ is being used, and if it is, what version is being enforced. There is no built-in cmdlet to do this. Someone else was able to point him to a function that did what he was looking for, but that function’s author said that it would only work for the global scope. In all honesty, that should be enough to cover virtually any use case you have for the function, but I was curious as to whether or not I could get the current scope’s setting, which could be different than the global scope’s.

The original Get-StrictMode function I found was using a function the author had picked up from PoshCode: Get-Field. It’s a function that gets public and private fields from objects passed to it–information that isn’t generally available. This is the function that I fell in love with.

I modified the Get-Field function somewhat, and you can find my version here.

Using the original Get-StrictMode as a starting point, I came up with my own version that can be found here.

And here’s a ridiculous example that shows that it works with multiple scopes:

function StrictModeTest {
    [CmdletBinding()]
    param([int] $Recurse = 0)

    Write-Verbose "Effective strict mode: $(Get-StrictMode)"

    switch (Get-Random -Minimum -1 -Maximum ($PSVersionTable.PSVersion.Major + 1)) {
        { $_ -gt -1 } { Write-Verbose "Setting strict mode to $_" }

        # This sets strict mode to 0.0
        0 { Set-StrictMode -Off }
        
        # Set strict mode to random version:
        { $_ -gt 0 } { Set-StrictMode -Version $_ }
    }
    Write-Verbose "New effective strict mode: $(Get-StrictMode)"

    if ($Recurse -le 0) { 
        Get-StrictMode -ShowAllScopes 
    }
    else { 
        if ($VerbosePreference -eq "Continue") {
            Get-StrictMode -ShowAllScopes
        }
        StrictModeTest -Recurse (--$Recurse) 
    }
}

Make sure that you’ve dot-sourced the Get-StrictMode function from the repository and run the StrictModeTest function definition above, then run the following command:


PS> StrictModeTest -Recurse 4

StrictModeVersion Scope
----------------- -----
2.0                   0
1.0                   1
3.0                   2
0.0                   3
                      4
2.0                   5

What the test function is doing is setting a random strict mode, then calling itself again. If you pass -Recurse the number 4, it will call itself 4 times, which means 5 scopes total (4 from the function calls, and 1 for the global scope). The last time the function calls itself, it calls Get-StrictMode with the -ShowAllScopes switch. If -Verbose is passed, it will call Get-StrictMode each time the function is called.

In my sample output, 5 is the global scope. Strict mode was set at 2.0 in that scope. Scope 4 didn’t have Set-StrictMode called, so it would inherit 2.0 from its parent scope. Scope 3 called ‘Set-StrictMode -Off’, so strict mode would not be enforced in that scope.

I don’t know that I’ll ever use Get-StrictMode, but I learned a ton playing around with it. Even if you don’t use it, you might take a look at it as an example of how to use Get-Field, which is one of my new favorite functions. I really think Get-Field has a ton of uses.

There’s still some work that can be done with both functions, so feel free to modify them and share your results.

In my previous post, I briefly mentioned that the VersionInfo.FileVersion property of a file object might not be accurate. I didn’t really give any examples to back that up, though. I’d like to show you how you can see this issue for yourself. First, run this so you can follow along in the examples:

$Culture = (Get-Culture).Name
$Files = dir C:\Windows\system32 -File | select Name, @{
        N="FileVersionString"; E={
            $String = $_.VersionInfo.FileVersion
            if ($String -match "(\d+\s*(\.|,)\s*){1,3}\d+") {
                $String = $Matches[0] -replace ",", "."
                $String = $String -replace "\s*", ""
                $String = ([version] $String).ToString()
            }
            $String
        }}, @{
        N="FileVersion";E={
            "{0}.{1}.{2}.{3}" -f $_.VersionInfo.FileMajorPart, 
                $_.VersionInfo.FileMinorPart, 
                $_.VersionInfo.FileBuildPart, 
                $_.VersionInfo.FilePrivatePart}
        }, @{
        N="MUIExists";E={
            Test-Path ("{0}\{1}\{2}.mui" -f $_.PSParentPath, $Culture, $_.Name)
        }}

This is a simple set of commands that gets all of the files from the system32 directory. For each file, it tries to get the file version information. The property named ‘FileVersionString’ takes the VersionInfo.FileVersion and does the following to it:
1. It removes any text as long as the string starts with something that resembles a version (spaces are allowed between the sections, and a comma or a decimal point can be used to separate the parts)
2. It replaces any commas with decimal points
3. It removes any spaces that might be left
4. It temporarily casts it to a [version] type, then back to a string. The idea here is to get rid of extra leading zeros, e.g., 9.00.7600.45000 becomes 9.0.7600.45000

The ‘FileVersion’ property is a string computed the way that I think is the best way to get a file version: combining the FileMajorPart, FileMinorPart, FileBuildPart, and FilePrivatePart numbers. The ‘MUIExists’ property is a Boolean property that tells you whether or not the file in question has a MUI resource file associated with it. The logic for this is very simple, and there may be some errors. Notice that the first line is getting the culture of the system (‘en-US’ on my computer).

Now lets look at the results:


# Show how many files were in folder:
PS> $Files.Count
3345

# Show how many files in folder have a VersionInfo.FileVersion string:
PS> ($Files | where { $_.FileVersionString }).Count
2944

# Get the files that have version strings that don't match the version numbers:
PS> $FilesWithNonMatchingVersions = $Files | 
    where { $_.FileVersionString -and $_.FileVersionString -ne $_.FileVersion }

# Show how many files have version information mismatches:
PS> $FilesWithNonMatchingVersions.Count
176

# Show how many files that have conflicting version information that have MUI 
# resources, and how many don't:
PS> $FilesWithNonMatchingVersions | group MUIExists -NoElement

Count Name                     
----- ----                     
  162 True                     
   14 False                    

There are 3345 files in the system32 directory on the machine I ran that on. Out of those, 2944 have FileVersion strings. Out of those, 176 have a mismatch between the VersionInfo.FileVersion property and the file version that was computed by combining the four version parts.

Out of those 176, 162 have a MUI resource file, and 14 don’t. The 14 that don’t are files where the version strings simply don’t match. The version that Explorer would show for those files is the version computed using the four version parts.

176 files in the system32 directory are showing the VersionInfo.FileVersion property from a different file than you think they are (it’s actually coming from the file’s MUI resource). That’s about 5.5% of the files that have a version in the system32 directory (on a Windows 7 system that I ran this on, it was 478 out of 2480, or just over 19%).

If you run these commands, you’ll get an example of a single random file (NOTE: this will copy a file into your temp directory):

$RandomFileName = $FilesWithNonMatchingVersions | ? { $_.MUIExists } | 
    Get-Random | select -exp Name
$SourceFile = "c:\windows\system32\$RandomFileName"
$MuiFile = "c:\windows\system32\$Culture\$RandomFileName.mui"
$DestFile = "$env:temp\$RandomFileName"
Copy-Item $SourceFile $DestFile
Get-Item $SourceFile, $MuiFile, $DestFile | select -exp VersionInfo | 
    fl FileName, FileVersion, FileM*Part, FileBuildPart, FilePrivatePart

And here are the results on the machine I used:


FileName        : C:\windows\system32\WMPhoto.dll
FileVersion     : 6.3.9600.16384 (winblue_rtm.130821-1623)
FileMajorPart   : 6
FileMinorPart   : 3
FileBuildPart   : 9600
FilePrivatePart : 16474

FileName        : C:\windows\system32\en-US\WMPhoto.dll.mui
FileVersion     : 6.3.9600.16384 (winblue_rtm.130821-1623)
FileMajorPart   : 6
FileMinorPart   : 3
FileBuildPart   : 9600
FilePrivatePart : 16384

FileName        : C:\Users\me\AppData\Local\Temp\WMPhoto.dll
FileVersion     : 6.3.9600.16474 (winblue_gdr.131122-1506)
FileMajorPart   : 6
FileMinorPart   : 3
FileBuildPart   : 9600
FilePrivatePart : 16474

Notice how on the first object the FileVersion doesn’t match the version if you combine the four numeric parts? Also, did you notice how the third object is showing matching versions? The binary file actually contains the proper information, but the API call that gets the file information is checking to see if there is MUI file, and if there is, it uses the FileVersion from the MUI file instead of the file you asked for. The file information from the first and third objects are from the exact same file, just in different directories. When the file is in the system32 directory, it has a MUI file associated with it. When it’s moved to the temp folder, it doesn’t. The four numeric file version parts are actually pulled from the real file whether or not there is a MUI file associated with it.

So, if you want accurate file versions (as accurate as Explorer will show you), I suggest using the four file version parts from the VersionInfo object: FileMajorPart, FileMinorPart, FileBuildPart, FilePrivatePart.

An example of doing that can be found here.

I used to think that getting a file version in PowerShell was a fairly simple process:


PS> (Get-Item c:\windows\system32\mshtml.dll).VersionInfo.FileVersion
11.00.9600.16410 (winblue_gdr.130923-1706)

There are two issues with that method, though:
1. That property can contain text along with the version
2. It turns out that in Windows Vista and higher, that property might not be coming from the file you think it is.

Issue #1 just means that the property is only good if you’re visually checking the version, or if no comparisons with other versions are necessary.

Issue #2 is the big one for me. What appears to be going on is that the FileVersion string is considered a non-fixed part of the version information, and it is pulled from the MUI resource file (if one exists). This file contains the language specific settings, and it looks like it only gets updated when the major or minor file version of associated file changes. If the private file version changes, it doesn’t look like the MUI file is changed.

That means that the FileVersion property has the potential to give you information that isn’t completely up to date. Critical patches from MS usually just update vulnerable code, so there’s no need to change language-specific things in the MUI file.

So, how can you get accurate file version information? Well, you can combine other properties from the VersionInfo object to create an accurate file version:


PS> $VersionInfo = (Get-Item C:\Windows\System32\mshtml.dll).VersionInfo
PS> $FileVersion = ("{0}.{1}.{2}.{3}" -f $VersionInfo.FileMajorPart, 
    $VersionInfo.FileMinorPart, 
    $VersionInfo.FileBuildPart, 
    $VersionInfo.FilePrivatePart)
PS> $FileVersion
11.0.9600.16476

That’s still pretty simple, but it takes a lot more typing. Because of that extra typing, though, your version is now in a known format, i.e., ‘x.x.x.x’. And, if you cast it to a [version] type, you can compare versions as if they were numbers:


PS> [version] $FileVersion -gt "11.0.9600.16410"
True

There is one thing to note here:
Version comparisons between different formats, e.g., 1.2.3 and 1.2.3.0, might not return what you’re expecting. See the remarks section here. Here are some examples of what I’m talking about:


# Gotcha #1
PS> [version] "1.2.3" -eq [version] "1.2.3.0"
False

# Gotcha #2
PS> [version] "1.2.3" -lt [version] "1.2.3.0"
True

# Example of different formats working as you would expect
PS> [version] "1.2.3" -gt [version] "1.2.2.15"
True

# Another example of different formats working
PS> [version] "1.3" -gt [version] "1.2.2.15"
True

As long as you keep your version formats the same, though, the comparison operators will work exactly as you would expect them to. I recommend keeping them in the ‘x.x.x.x’ format.

Something else about the comparisons: In the examples, casting the second string to a [version] isn’t actually necessary. When a comparison operation occurs in PowerShell, the type of the first object is used for the comparisons. That means that the second string in the examples is automatically converted to a [version], even if you don’t tell PowerShell to do it. If you run this command, you’ll get an error:


PS> [version] "1.2.3.4" -gt "potato"
Could not compare "1.2.3.4" to "potato". Error: "Cannot convert value "potato" to type "System.Version". Error: "Version string portion was too short or too long.""
At line:1 char:1
+ [version] "1.2.3.4" -gt "potato"
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : InvalidOperation: (:) [], RuntimeException
    + FullyQualifiedErrorId : ComparisonFailure

I just put a string there, but PowerShell tried to make it a [version] object. It doesn’t hurt to always explicitly use the type (you could even argue that it’s a best practice), but it’s not necessary if you’re trying to cut down on the characters you have to type. Just make sure that the first object is actually a [version] type.

Like I said before, the biggest drawback to combining the four File*Part properties is how much typing it requires. So, I decided to make PowerShell always get the file version for me, and automatically add it to a file object:

# PowerShell must be at least v3 to do this:
Update-TypeData -TypeName System.IO.FileInfo -MemberType ScriptProperty -MemberName PSFileVersion -Value {
    if ($this.VersionInfo.FileVersion) {
        [version] ("{0}.{1}.{2}.{3}" -f $this.VersionInfo.FileMajorPart, 
            $this.VersionInfo.FileMinorPart, 
            $this.VersionInfo.FileBuildPart, 
            $this.VersionInfo.FilePrivatePart)
    }    
}

That method requires version 3.0. If you have 2.0, you should be able to get it to work, though. The difference is that you have to save an XML file, and then use the Update-TypeData cmdlet to read that file. Here’s the XML file:

<Types>
    <Type>
        <Name>System.IO.FileInfo</Name>
        <Members>
            <ScriptProperty>
                <Name>PSFileVersion</Name>
                <GetScriptBlock>
                    if ($this.VersionInfo.FileVersion) {
                        [version] ("{0}.{1}.{2}.{3}" -f $this.VersionInfo.FileMajorPart, 
                            $this.VersionInfo.FileMinorPart, 
                            $this.VersionInfo.FileBuildPart, 
                            $this.VersionInfo.FilePrivatePart)
                    }    
                </GetScriptBlock>
            </ScriptProperty>
        </Members>
    </Type>
</Types>

And here’s the command:

# Replace $PathToXmlFile with proper path:
Update-TypeData -AppendPath $PathToXmlFile

This works on my machine when forcing PowerShell to run version 2, but I haven’t tested it on a machine that truly has PowerShell v2 installed.

Now, when you use Get-Item, Get-ChildItem, or anything that returns a [System.IO.FileInfo] object, the object will have a PSFileVersion property. Here are some examples of this functionality:


# Use Get-ChildItem to display file names and versions:
PS> dir C:\Windows\system32 -Filter msh*.dll | ? psfileversion | select name, psfileversion

Name          PSFileVersion  
----          -------------  
mshtml.dll    11.0.9600.16476
MshtmlDac.dll 11.0.9600.16384
mshtmled.dll  11.0.9600.16384
mshtmler.dll  11.0.9600.16384


# Same as before, but this shows the accurate System.Version version and the inaccurate VersionInfo.FileVersion:
PS> dir C:\Windows\system32 -Filter msh*.dll | ? psfileversion | select name, psfileversion, @{N="FileVersionString";E={$_.VersionInfo.FileVersion}}

Name          PSFileVersion   FileVersionString                         
----          -------------   -----------------                         
mshtml.dll    11.0.9600.16476 11.00.9600.16410 (winblue_gdr.130923-1706)
MshtmlDac.dll 11.0.9600.16384 11.00.9600.16384 (winblue_rtm.130821-1623)
mshtmled.dll  11.0.9600.16384 11.00.9600.16384 (winblue_rtm.130821-1623)
mshtmler.dll  11.0.9600.16384 11.00.9600.16384 (winblue_rtm.130821-1623)


# Comparisons:
PS> (Get-Item C:\Windows\System32\mshtml.dll).PSFileVersion -gt "11.0.9600.16410"
True
PS> (Get-Item C:\Windows\System32\mshtml.dll).PSFileVersion -lt "11.0.9600.16410"
False


# List all of the files with versions under system32:
dir C:\Windows\System32 | ? psfileversion | select name, psfileversion | Out-GridView

You can find this on the Script Repository here.

Today I want to go over one of the functions in the module that I published to the Script Center: Get-AccessControlEntry.

This function is meant to be used for auditing of access control entries (ACEs) in access control lists (ACLs). Right now, it is geared towards discretionary ACLs (DACLs), or the ACLs that control access to objects. It will work with system ACLs (SACLs), or the ACLs that control auditing of objects, in objects returned from Get-Acl with the -Audit switch, but the ACEs returned have at least one different property name from an ACE in a DACL, so the default formatting of the results doesn’t work properly. For now, I suggest just using it to audit DACLs. All of the other functions that deal with ACLs and ACEs in the module are currently geared towards DACLs, but I plan to fix that in a future release if anyone besides myself shows interest. When/if that happens, I’ll have hopefully come up with a solution that allows this function to better work with SACLs.

So, how does it work? The short answer is that it expands the ACE objects found in the ACLs of a security descriptor (SD) object, and it adds the path of the original object to each ACE. It really becomes useful when you feed it more than one object, and you get all of the ACEs expanded. I’ll go over some examples of that after covering the command syntax.

Let’s see how to use it:


PS> Get-Command Get-AccessControlEntry -Syntax

Get-AccessControlEntry [-Path ] [-AclType ] [-Filter ] [-Recurse] 
[-AceInherited] [-AceNotInherited] []

Get-AccessControlEntry [-AclObject ] [-AclType ] [-Filter ] [-AceInherited] 
[-AceNotInherited] []

So, there are two ways to call the function: you can pass it a path, or you can directly pass it an SD object. Let’s go over each of the parameters unique to each ParameterSet, then we’ll cover the common parameters that you can use for either type of call.

  • ByPath Parameters:
    • Path – A path to an object that will work with Get-Acl. Anything passed to this parameter is simply passed to Get-ChildItem, and Get-Acl is called on the object(s) returned.
    • Recurse – Passes the -Recurse switch to Get-ChildItem for the path provided in the -Path parameter.
  • ByAclObject Parameters:
    • AclObject – A security descriptor object. The ACEs will be extracted from properties named in the -AclType parameter (if the -AclType parameter isn’t specified, the default values are used)
  • Common Parameters:
    • AclType – This is a list of properties to attempt to expand from the AclObjects. By default, the list contains the following strings:
      • Access – The property on a security descriptor returned from Get-Acl that contains the DACL
      • Audit – The property on a security descriptor returned from Get-Acl that contains the SACL
      • DiscretionaryAcl – The property on a security descriptor object returned from New-AdaptedSecurityDescriptor that contains the DACL
      • SystemAcl – The property on a security descriptor object returned from New-AdaptedSecurityDescriptor that contains the SACL
    • Filter – A script block that filters the ACEs. The script block must evaluate to true in order for the ACE to be returned. You can use $_ to refer to the ACE object.
    • AceInherited and AceNotInherited – These switches add on to the script block defined in the -Filter parameter to control which ACEs are displayed. They either show only ACEs that are inherited or only ACEs that aren’t inherited.

Here is a very brief demo script. Run a few of these commands to see a sampling of what you can do with the function:

# Show the ACEs on the Windows folder (format results as a table):
Get-Acl C:\Windows | Get-AccessControlEntry | ft

# Same thing, but pipe the results to Out-GridView so you can sort 
# them as you please:
Get-Acl C:\Windows | Get-AccessControlEntry | Out-GridView

# Get the ACEs for any IdentityReferences that have the word 'Users' 
# in them:
Get-Acl C:\Windows | Get-AccessControlEntry -Filter { 
    $_.IdentityReference -match "Users" 
}

# Files under the Windows folder, only showing inherited
# ACEs (different display this time):
dir C:\Windows -File | 
    % { Get-Acl $_.PsPath } | 
    Get-AccessControlEntry -AceInherited |
    Sort-Object Path, AccessControlType, IdentityReference |
    Format-Table -GroupBy Path -Property AccessControlType, IdentityReference, FileSystemRights

# If you change -AceInherited above to -AceNotInherited, you'll get a 
# listing of all ACEs that aren't inherited.

# View the restriction policy for component access ACEs:
New-AdaptedSecurityDescriptor -BinarySD (gp HKLM:\SOFTWARE\Microsoft\Ole).MachineAccessRestriction |
    Get-AccessControlEntry |
    Out-GridView

<# View the namespace security on the root/cimv2 WMI namespace:
   NOTE: This must be run from an elevated prompt. Also, there is
         a bug in the WMI methods that are used to convert the 
         Win32SD to SDDL or binary forms that causes the
         inheritance flags to not show properly. That's a pretty
         big issue, and I plan to take that into account if anyone
         uses the module. #>
Get-CimInstance __SystemSecurity | 
    Get-Win32SecurityDescriptor -Sddl | 
    New-AdaptedSecurityDescriptor | 
    Get-AccessControlEntry |
    Out-GridView

# Use this to get a list of WMI classes that you can use 
# Get-Win32SecurityDescriptor against:
Get-CimClass -MethodName GetSecurityDescriptor

# Get WSMan ACEs:
dir WSMan:\localhost -Recurse | 
    Where Name -eq Sddl | 
    ForEach-Object { 
        New-AdaptedSecurityDescriptor -Sddl $_.Value -Path $_.PsPath -AccessMaskEnumeration ([PowerShellAccessControl.WsManAccessRights])
    } |
    Get-AccessControlEntry |
    Sort-Object Path |
    Format-Table -GroupBy Path -AutoSize

Try it out, and let me know what you do or don’t like about it.

This is a function from my PowerShellAccessControl module that is used to take the SDDL or binary form of a security descriptor (SD) as input and output an object that resembles an SD from Get-Acl. Right off the bat, let me say that this thing is missing a lot of functionality. It currently only works with discretionary ACLs (the ACLs that control access to objects). Also, the script methods that it exposes aren’t very discoverable via Get-Member since they were added with Add-Member. This thing really deserves to have a true C# object be its output, and I’ll probably go in that direction at some point in the future.

Even with its flaws, I still think it’s a very useful function. Here’s the functionality of a Get-Acl SD that it currently mimics:

  • Access property lists each ACE
  • AccessToString property lists the value of the Access property in a single string
  • Sddl property gives the SDDL representation of the entire SD object
  • GetSecurityDescriptorBinaryForm method gives the binary form of the entire SD object
  • AddAccessRule method takes an ACE as input and adds it to the discretionary ACL
  • RemoveAccessRule takes either an index to an ACE or an ACE object and removes it from the discretionary ACL

So, if you can get to either the SDDL or binary form of a SD, you can pass that to this function and get an object that is much more readable, and that has the ability to change the discretionary ACL.

Besides the two SD input parameters (SDDL or BinarySD; you can only use one at a time), the function has two parameters: AccessMaskEnumeration and Path.

The AccessMaskEnumeration is an optional parameter that the output object can use to translate the access rights of the object into a readable string. In an SD, all access rights are stored as a bitmask. If you don’t have an enumeration to do the translating of rights, you’ll just see an integer in each ACE where the access rights should go. You’ll still be able to see what users/groups have rights to the object, but you won’t know what rights they have (unless you know what the numeric values mean). The function comes with several enumerations for different types of objects, and I’m going to devote a blog post to creating one for printers to show how you can easily make your own for future use. Here is a list of the enumerations that the module comes with:


PS> [System.AppDomain]::CurrentDomain.GetAssemblies().GetTypes() | 
Where-Object FullName -match "^PowerShellAccessControl\."

IsPublic IsSerial Name                                     BaseType
-------- -------- ----                                     --------
True     True     LogicalShareRights                       System.Enum
True     True     WmiNamespaceRights                       System.Enum
True     True     WsManAccessRights                        System.Enum
True     True     ServiceAccessRights                      System.Enum 

The Path parameter is another optional parameter. It gives you the ability to look at the output SD object and tell where it came from. This is very useful when you have more than one SD object and you use the Get-AccessControlEntry function from the module.

Let’s go over some examples!

Here’s a way to get the WMI namespace access rights for the root\cimv2 namespace (NOTE: I’m using the GetSD WMI method on the class directly instead of a different function that I included in the module called Get-Win32SecurityDescriptor. I’ll explain why I used this WMI method directly when I cover the Get-Win32SecurityDescriptor function).


PS> # This should be run from an elevated PS prompt:
PS> $BinarySD = Get-CimClass __SystemSecurity -Namespace root\cimv2 | 
Invoke-CimMethod -MethodName GetSD | 
Select-Object -ExpandProperty SD

PS> $SD = New-AdaptedSecurityDescriptor -BinarySD $BinarySD -Path "root\CIMV2 NameSpace"

PS> $SD.Access | ft

ObjectRights AccessControlType IdentityReference                IsInherited InheritanceFlags PropagationFlags
------------ ----------------- -----------------                ----------- ---------------- ----------------
      393279     AccessAllowed BUILTIN\Administrators                  True ContainerInherit             None
          19     AccessAllowed NT AUTHORITY\NETWORK SERVICE            True ContainerInherit             None
          19     AccessAllowed NT AUTHORITY\LOCAL SERVICE              True ContainerInherit             None
          19     AccessAllowed NT AUTHORITY\Authenticated Users        True ContainerInherit             None

PS> # Notice the ObjectRights listed are numeric. Let's try this again with an enumeration:

PS> $SD = New-AdaptedSecurityDescriptor -BinarySD $BinarySD -Path "root\CIMV2 Namespace" -AccessMaskEnumeration ([PowerShellAccessControl.WmiNamespaceRights])

PS> $SD.Access | ft # Some columns are dropped:

                                ObjectRights AccessControlType IdentityReference                IsInherited
                                ------------ ----------------- -----------------                -----------
... RemoteEnable, ReadSecurity, EditSecurity     AccessAllowed BUILTIN\Administrators                  True
EnableAccount, ExecuteMethods, ProviderWrite     AccessAllowed NT AUTHORITY\NETWORK SERVICE            True
EnableAccount, ExecuteMethods, ProviderWrite     AccessAllowed NT AUTHORITY\LOCAL SERVICE              True
EnableAccount, ExecuteMethods, ProviderWrite     AccessAllowed NT AUTHORITY\Authenticated Users        True

Show the ACEs for the microsoft.powershell session configuration:


PS> # This should be run from an elevated PS prompt
PS> dir WSMan:\localhost\Plugin\microsoft.powershell\Resources -Recurse | 
Where Name -eq Sddl | 
ForEach-Object { 
    New-AdaptedSecurityDescriptor -Sddl $_.Value -Path $_.PsPath -AccessMaskEnumeration ([PowerShellAccessControl.WsManAccessRights])
} | 
Select -exp Access

ObjectRights      : Full
AccessControlType : AccessAllowed
IdentityReference : BUILTIN\Administrators
IsInherited       : False
InheritanceFlags  : None
PropagationFlags  : None

ObjectRights      : Full
AccessControlType : AccessAllowed
IdentityReference : BUILTIN\Remote Management Users
IsInherited       : False
InheritanceFlags  : None
PropagationFlags  : None

Show the AccessToString property for all of the shares on a remote computer named ‘server’ (I’m using another function from the module that I will devote a blog post to soon):


PS> Get-CimInstance -ClassName Win32_LogicalShareSecuritySetting -ComputerName server | 
Get-Win32SecurityDescriptor -Sddl |
New-AdaptedSecurityDescriptor -AccessMaskEnumeration ([PowerShellAccessControl.LogicalShareRights]) | 
Select Path, AccessToString |
fl

Path           : \\server\root\cimv2:Win32_LogicalShareSecuritySetting.Name="share01"
AccessToString : Everyone AccessAllowed FullControl
                 BUILTIN\Users AccessAllowed Read
                 

Path           : \\server\root\cimv2:Win32_LogicalShareSecuritySetting.Name="share02"
AccessToString : BUILTIN\Administrators AccessAllowed FullControl
                 Everyone AccessAllowed FullControl

Show all ACEs from any object named Sddl in the local WSMan configuration (I’m using another function that will be described later this week; you’ll have to run this and see the results):


PS> # This should be run from an elevated PS prompt
PS> dir WSMan:\localhost -Recurse | 
Where Name -eq Sddl | 
ForEach-Object { 
    New-AdaptedSecurityDescriptor -Sddl $_.Value -Path $_.PsPath -AccessMaskEnumeration ([PowerShellAccessControl.WsManAccessRights])
} | 
Get-AccessControlEntry |
Out-GridView

So, if you can get access to a hard to read SDDL form or an impossible to read binary form of an SD, you should be able to turn it into something that’s readable with New-AdaptedSecurityDescriptor. If you have an enumeration that translates the object rights into a readable form, that’s even better (but not necessary). You can then use that object to audit and/or modify the SD (those are for another day).

I hope that you find this function useful. Please try it out and tell me what you think. Stay tuned for more posts on the module, including more on this function!

I’ve created a PowerShell module that contains several functions that (I think) make working with security descriptors in PowerShell a little bit easier. Here are the functions that are currently included:

Get-AccessControlEntry
New-AdaptedSecurityDescriptor
Get-Win32SecurityDescriptor
New-AccessControlEntry
ConvertTo-Win32SecurityDescriptor

Over the coming days, I’ll have some blogs posts dedicated to each of the functions to show how they are used. I’ll also go over some of the features that are still missing that I plan to add over time.

If you take the time to look at it and find something that doesn’t appear to work properly, or if you find a feature that you feel is missing, please let me know and I will look into it.

I know I’ve been quiet on here for the last several weeks, but I am actually working on some posts. Today I wanted to mention an obscure problem that I cam across the other day.

So, what was my problem? Well, I’m planning on releasing a group of functions that I’ve been working on that will allow you to work with almost any type of security descriptors (SDs) that you can get your hands on (SDs for files, folders, registry keys, printers, services, shares, WMI namespaces, PS configuration files, etc). I’m going to post it on the TechNet Script Repository, and I’ll have a series of blog posts detailing how to use each function, and how to work with some of the SDs.

I mention all of that to setup the problem: the function that gets SDs using WMI/CIM needed a way to uniquely represent the instance that the SD belongs to. If you call Get-Acl on a file, you’ll notice that the object returned contains a path property along with the security descriptor info. If you get SDs for several files at once, then try to get a list of all of the access control entries (ACEs) contained on all of them, the path helps you keep track of what object each ACE belongs to. If you use WMI to get the SDs for printers, network shares, etc from multiple computers at once, it helps to have an identifier to attach to each of the objects returned, just like Get-Acl does with files. I’ll go into this in a lot more detail in a future blog post.

If you call Get-WmiObject, the perfect unique identifier is located in the __PATH property on each object returned. As far as I can tell, the Get-CimInstance cmdlet doesn’t give this information (more specifically, the [Microsoft.Management.Infrastructure.CimInstance] object returned by the cmdlet doesn’t have this information available). It turns out that it has a place holder for the information in the ‘CimSystemProperties’ collection that each instance contains, but that property isn’t currently implemented.

Jeff Hicks presented at the Mississippi PowerShell User Group this past Tuesday, and, after his awesome presentation, I mentioned the issue to him and showed him how I was getting around it. He has a really good blog post working through the issue, and he created a really good helper function to add the __PATH property to a CimInstance object. I recommend taking a look at it before continuing.

After I came up with a way to get the WMI path information from a CimInstance, I did think of one other alternate use for it: you can convert a CimInstance into a ManagementObject (that’s what Get-WmiObject returns). I’m not exactly sure why you’d want to do it, but you can :). Here’s my function that gets the path information:

#requires -version 3.0
function Get-CimPathFromInstance {

    [CmdletBinding()]
    param(
        [Parameter(Mandatory,ValueFromPipeline)]
        [ciminstance] $InputObject
    )

    process {
        $Keys = $InputObject.CimClass.CimClassProperties | 
            Where-Object { $_.Qualifiers.Name -contains "Key" } |
            Select-Object Name, CimType | 
            Sort-Object Name

        $KeyValuePairs = $Keys | ForEach-Object { 

            $KeyName = $_.Name
            switch -regex ($_.CimType) {

                "Boolean|.Int\d+" {
                    # No quotes surrounding value:
                    $Value = $InputObject.$KeyName
                }

                "DateTime" {
                    # Conver to WMI datetime
                    $Value = '"{0}"' -f [System.Management.ManagementDateTimeConverter]::ToDmtfDateTime($InputObject.$KeyName)
                }

                "Reference" {
                    throw "CimInstance contains a key with type 'Reference'. This isn't currenlty supported (but can be added later)"
                }

                default {
                    # Treat it like a string and cross your fingers:
                    $Value = '"{0}"'  -f ($InputObject.$KeyName -replace "`"", "\`"")
                }
            }

            "{0}={1}" -f $KeyName, $Value
        }

        if ($KeyValuePairs) { 
            $KeyValuePairsString = ".{0}" -f ($KeyValuePairs -join ",")
        }
        else {
            # This is how WMI seems to handle paths with no keys
            $KeyValuePairsString = "=@" 
        }

        "\\{0}\{1}:{2}{3}" -f $InputObject.CimSystemProperties.ServerName, 
                               ($InputObject.CimSystemProperties.Namespace -replace "/","\"), 
                               $InputObject.CimSystemProperties.ClassName, 
                               $KeyValuePairsString


    }
}

And here’s how you can convert a CimInstance into a ManagementObject:


PS> Get-CimInstance -Query "SELECT * FROM Win32_PingStatus WHERE Address='127.0.0.1'" | 
>> Get-CimPathFromInstance | Tee-Object -Variable Path

\\client01\root\cimv2:Win32_PingStatus.Address="127.0.0.1",BufferSize=32,NoFragmentation=False,
RecordRoute=0,ResolveAddressNames=False,SourceRoute="",SourceRouteType=0,Timeout=4000,Timestamp
Route=0,TimeToLive=128,TypeofService=0

PS> [wmi] $Path

Source        Destination     IPV4Address      IPV6Address                              Bytes    Time(ms) 
------        -----------     -----------      -----------                              -----    -------- 
client01      127.0.0.1       192.168.10.15    fe80::16a:e187:f957:f77c%13              32       0        

If you’ve got a way to convert it one way, you need a way to convert it the other way, too:

#requires -version 3.0
function Get-CimInstanceFromPath {

    [CmdletBinding()]
    param(
        [Parameter(Mandatory, ValueFromPipelineByPropertyName, ValueFromPipeline)]
        [Alias('__PATH')]
        [string] $Path
    )

    process {
        if ($Path -match "^\\\\(?<computername>[^\\]*)\\(?<namespace>[^:]*):(?<classname>[^=\.]*)(?<separator>\.|(=@))(?<keyvaluepairs>.*)?$") {
            $Query = "SELECT * FROM {0}" -f $matches.classname

            switch ($matches.separator) {

                "." {
                    # Key/value pairs are in string, so add a WHERE clause
                    $Query += " WHERE {0}" -f [string]::Join(" AND ", $matches.keyvaluepairs -split ",")
                }
            }

            $GcimParams = @{
                ComputerName = $matches.computername
                Query = $Query
                ErrorAction = "Stop"
            }

        }
        else {
            throw "Path not in expected format!"
        }

        Get-CimInstance @GcimParams
    }

}

PS> $WmiBios = Get-WmiObject Win32_BIOS

PS> $WmiBios.__PATH | Get-CimInstanceFromPath | Tee-Object -Variable CimBios

SMBIOSBIOSVersion : xxx
Manufacturer      : Dell Inc.
Name              : Default System BIOS
SerialNumber      : xxxxxxx
Version           : DELL   - 1
PSComputerName    : client01

PS> $CimBios.GetType().FullName
Microsoft.Management.Infrastructure.CimInstance


PS> # You can also just pipe a ManagementObject directly to it
PS> Get-WmiObject Win32_BIOS | Get-CimInstanceFromPath

SMBIOSBIOSVersion : xxx
Manufacturer      : Dell Inc.
Name              : Default System BIOS
SerialNumber      : xxxxxxx
Version           : DELL   - 1
PSComputerName    : client01

Again, I’m not sure why you’d want to do it, but I realized you could, so I wanted to put it out there. If for some reason this is of any value to you, just be warned that there’s a good chance you’ll find instances where you get an error casting the CIM path string into a [wmi] ManagementObject. This is because there’s a good chance that the path isn’t always properly formed.

If you do actually have a use for it, please leave a comment to let me know.

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.