Over the summer, the PowerShell Access Control module got some DSC resources to help manage security descriptors for for some of the supported object types. I’ve tested them a little bit, but I haven’t had enough time to really make sure they work as well as I’d like. Also, they’re still missing some functionality, and there are still some design decisions that haven’t been finalized. When I saw that the PowerShell Summit’s DSC Hackathon has a scenario for creating a resource that handles file and folder ACLs, I thought this would be a good time to show what’s currently in the module. I’m hoping that other people will test the resources and help me figure out what’s missing or what needs to be changed (I already know that the code needs to be cleaned up and the Get-TargetResource functions need some work).

If you download the latest version from the repository, you’ll see that the module includes three resources: cAccessControlEntry, cSecurityDescriptorSddl, and cSecurityDescriptor. Each is described in a little more detail below.

NOTE: The types of securable objects that these work against is currently limited to Files, Folders, Registry Keys, WMI Namespaces, and Services. The only reason the other object types that the module supports won’t work is that I haven’t really documented the path format. I mention that a little bit below when describing each of the properties for the cAccessControlEntry resource. Look for more supported objects in a future release, especially Active Directory objects.

cAccessControlEntry

The first of the three resources provides the least amount of control over a security descriptor. cAccessControlEntry provides a way to check that a DACL contains (or doesn’t contain) certain access or that a SACL contains (or doesn’t contain) entries that will generate certain audits. Here are a few scenarios that you can use it for:

  • Make sure Users group has Modify rights to a folder, but not any of its sub folders and files
  • Make sure Users group doesn’t have Delete right to a file or folder
  • Make sure Users group is explicitly denied Delete right on a file or folder
  • Make sure Users will generate an audit when any failed access attempt is performed
  • Make sure Users have Start and Stop rights to a specific service

The resource has the following properties:

  • AceType (Required) – The type of ACE; options are AccessAllowed, AccessDenied, and SystemAudit
  • ObjectType (Required) – The type of the securable object. Currently limited to File, Directory, RegistryKey, Service, and WmiNamespace. The only difference between File and Directory is the default AppliesTo value (if you don’t specify AppliesTo, a File object will use Object and a Directory object will use Object, ChildContainers, ChildObjects)
  • Path (Required) – The path to the securable object. This is obvious for files, folders, and registry keys, but not necessarily for other object types. You can get the path to your securable object by using Get-SecurityDescriptor and copying the SdPath property.
  • Principal (Required) – User/group/etc that is being granted/denied access or audited.
  • AccessMask (Required unless Ensure is set to Absent) – An integer that specifies the access to grant/deny/audit.
  • Ensure (Optional) – Controls whether an ACE for the specified properties should be present or absent from the DACL/SACL.
  • AppliesTo (Optional) – This is only used when dealing with a container object (an object that can have children, like folders, registry keys, WMI namespaces). It allows you to control where the ACE will apply. If you don’t use it, the default is used, which may be different depending on the ObjectType.
  • OnlyApplyToThisContainer (Optional) – Used like AppliesTo. This sets the NoPropagateInherit propagation flag, which means that children of the object that Path points to will inherit the ACE described, but their children (the object’s grandchildren) will not.
  • Specific (Optional) – Makes sure that the ACE described by the supplied properties is exactly matched. For example, if you want to make sure Users have Read access, and they already have Modify, testing for the desired access would normally pass since Modify contains Read. If you supply a value of $true for this property, though, the test would fail since Modify is not the same as Read. If this was set to $true in the previous example, the Modify ACE would be removed and a new Read ACE would be added.
  • AuditSuccess and AuditFailure (Only valid when AceType is SystemAudit) – At least one of these properties must be set to $true when describing an audit ACE.

The resource will currently only check against explicitly defined ACE entries. That means that inherited entries are completely ignored. If you’re ensuring access is granted or denied, that shouldn’t be a problem, but it could be a problem if you want to make sure access isn’t granted (Ensure = Absent). Let me demonstrate with a few examples:

Example 1: Make sure Users group has Modify rights to c:\powershell\dsc\test folder and its subfolders (but not files)

First, lets look at the DACL before making any changes:

dsc_cAccessControlEntry_1

Notice that Users already has Modify rights, but they’re being inherited from the parent folder. If we run the following DSC configuration, a new explicit ACE will be added since the DSC resource ignores inherited ACEs:

configuration DscAceTest {
    param(
        [string[]] $ComputerName = "localhost"
    )

    Import-DscResource -Module PowerShellAccessControl

    cAccessControlEntry UsersModifyFolder {
        AceType = "AccessAllowed"
        ObjectType = "Directory"
        Path = "C:\powershell\dsc\test"
        Principal = "Users"
        AccessMask = [System.Security.AccessControl.FileSystemRights]::Modify
        AppliesTo = "Object, ChildContainers"  # Apply to the folder and subfolders only
    }
}

dsc_cAccessControlEntry_3

If you were to change the cAccessControlEntry node shown above to include Ensure = ‘Absent’, the DACL would go back to what it looked like in the first screenshot. The inherited ACE would still be there, though, and the LCM would tell you that the configuration was successfully applied (and Test-DscConfiguration would return $true).

Example 2: Make sure Users don’t have Delete rights on the folder itself (but don’t worry about sub folders or files)

For this example, we’ll actually pick up where the last one left off, so see the last screenshot. Users have an ACE that is not inherited that grants Modify rights to the folder and subfolders (Object and ChildContainers). Lets assume that we didn’t set that up with DSC (that just so happens to be what the folder’s DACL currently looks like), and we just want to make sure that Users can’t delete the folder. To do that, you could run the following configuration:

configuration DscAceTest {
    param(
        [string[]] $ComputerName = "localhost"
    )

    Import-DscResource -Module PowerShellAccessControl

    cAccessControlEntry UsersCantDeleteFolder {
        AceType = "AccessAllowed"
        ObjectType = "Directory"
        Path = "C:\powershell\dsc\test"
        Principal = "Users"
        AccessMask = [System.Security.AccessControl.FileSystemRights]::Delete
        AppliesTo = "Object"  # Only apply to the folder
        Ensure = "Absent"     # Make sure permission isn't granted
    }
}

And you’d get a DACL that looks like this:dsc_cAccessControlEntry_5

What happened there? When the configuration was run, the LCM saw that it needed to make some changes because Users had Delete permission to the folder object. When the configuration was applied, only Delete permissions were removed from the folder itself, so the single ACE needed to be split into two ACEs: one that gives Modify minus Delete to the folder and subfolders, and one that gives Delete to just the subfolders. In the end, the LCM did exactly what it was asked, which was ensure that Delete permission wasn’t granted to the folder itself.

Remember that there is still an inherited ACE that grants that permission to the Users group. To get around this, you’ll need to use the cSecurityDescriptorSddl or cSecurityDescriptor resources instead since they have the ability to control DACL and SACL inheritance.

cSecurityDescriptorSddl

This one is really simple to explain, but, since it uses SDDL, its kind of tough to use. You get to control a lot more with this resource than with cAccessControlEntry because this lets you control the entire security descriptor. There are only three properties, and they are all required:

  • Path – This is the same as the Path property for cAccessControlEntry above.
  • ObjectType – This is the same as the ObjectType property for cAccessControlEntry above.
  • Sddl – This is a string representation of the security descriptor. The neat thing about this is that you can include any combination of the four security descriptor sections: Owner, Group, DACL, or SACL. Any section that is missing from the SDDL string shouldn’t be tested or touched.

To use it, I recommend configuring an object the way you want it, and running the following to get the SDDL string:

# This is if you want the entire SD:
(Get-SecurityDescriptor C:\powershell\dsc\DscAceTest).Sddl

# If you only want certain sections, do this (the latest builds of the PAC
# module have this method exposed to the SD object itself, so this format 
# won't work in a future version without a slight modification)
# Valid arguments for the GetSddlForm() method are All, Owner, Group, Access,
# and Audit:
$SD = Get-SecurityDescriptor C:\powershell\dsc\DscAceTest
$SD.SecurityDescriptor.GetSddlForm("Owner, Access")

Let’s continue from the example above. We can’t control the ACEs that are being inherited (unless we modify the parent object), but we can tell the folder to disable DACL inheritance. The following configuration contains an SDDL string that only modifies the DACL (so the Owner, Group, and SACL aren’t touched), disables DACL inheritance, and specifies each of the ACEs that were being inherited as explicit entries instead:

configuration DscDaclTest {
    param(
        [string[]] $ComputerName = "localhost"
    )

    Import-DscResource -Module PowerShellAccessControl

    cSecurityDescriptorSddl TestDacl {
        ObjectType = "Directory"
        Path = "C:\powershell\dsc\test"
        Sddl = "D:PAI(A;OICIIO;SDGXGWGR;;;AU)(A;;0x1301bf;;;AU)(A;OICI;FA;;;SY)(A;OICI;FA;;;BA)(A;CI;0x1201bf;;;BU)(A;CIIO;SD;;;BU)"
    }
}

After running that, the folder’s DACL looked like this for me:

dsc_cAccessControlEntry_6

Using that resource means that you can force that DACL to look like it does above every single time the DSC configuration is run. cAccessControlEntry only cared about the specific ACE properties supplied to it, and it didn’t care about the rest of the ACL. This resource, when the DACL or SACL sections are specified, controls the whole ACL. That’s pretty powerful, but it’s really, really hard to read. Thankfully, the last resource fixes the readability part (I hope).

cSecurityDescriptor

This resource does the exact same thing as cSecurityDescriptorSddl, except it doesn’t use SDDL. It has the following properties:

  • Path – This is the same as the Path property for cAccessControlEntry above.
  • ObjectType – This is the same as the ObjectType property for cAccessControlEntry above.
  • Owner – A string specifying who/what the owner should be set to
  • Group – A string specifying who/what the group should be set to
  • Access – A CSV that specifies the explicit DACL ACEs that should be present. If the explicit ACEs don’t match this list, all explicit ACEs will be removed, and ACEs specified here will be applied. The headers are parameter names that would be passed to the New-AccessControlEntry function.
  • AccessInheritance – Controls DACL inheritance. Valid values are Enabled and Disabled.
  • Audit – A CSV that specifies the explicit SACL ACEs that should be present. If the explicit ACEs don’t match this list, all explicit ACEs will be removed, and ACEs specified here will be applied. The headers are parameter names that would be passed to the New-AccessControlEntry function.
  • AuditInheritance – Controls SACL inheritance. Valid values are Enabled and Disabled.

The following configuration does the same thing as the cSecurityDescriptorSddl example above:

configuration DscDaclTest {
    param(
        [string[]] $ComputerName = "localhost"
    )

    Import-DscResource -Module PowerShellAccessControl

        cSecurityDescriptor TestFolderSdDacl {
            Path = "c:\powershell\dsc\test"
            ObjectType = "Directory"
            AccessInheritance = "Disabled"
            Access = @"
                AceType,Principal,FolderRights,AppliesTo
                AccessAllowed,Authenticated Users,"Modify,Synchronize"
                AccessAllowed,SYSTEM,FullControl
                AccessAllowed,Administrators,FullControl
                AccessAllowed,Users,"Write,ReadAndExecute,Synchronize","Object,ChildContainers"
                AccessAllowed,Users,Delete,ChildContainers
"@
        }
}

If you download the module, there are more examples of each of the resources in the \examples\dsc\ folder. Please grab the latest version and give these resources a try. If you have any questions, suggestions, or criticisms, please leave a comment below and let me know. Thanks!

I’ve had a beta version of the PAC module available on Script Center repository for quite a while now. It adds several new features, including the following:

  • Active Directory objects are supported
  • Desired State Configurations (DSC) resources available to automate access control settings (see about_PowerShellAccessControl_DscResources)
  • Supports filenames longer than 260 characters (PSv3 or higher)
  • Shows inheritance source for file, folder, registry key, and Active Directory objects (PSv3 or higher)

The documentation isn’t finished, and there are still a few bugs that I’m aware of that need to be fixed. I’ve been using it in its current form for a while, though, so I feel that it’s pretty stable. Give it a shot and let me know if you have any issues and/or questions (you can post here or on the Q&A section on the repository page.

The biggest problem I have with it is the speed (especially when working with AD objects). I’ve been playing around with moving some of it to C#, and I’ve noticed an amazing speed improvement. At some point in the future, the module will have at least some C#. I may one day make the entire module a compiled module (the source code will always be included).

Other features that will come in the future:

  • Central Access Policies (view/set assigned CAPs for files and folders and view central access rules associated with the CAP)
  • Conditional/Callback ACEs will show conditions (very similar to ACL Editor)
  • File/folder dynamic access control tags will be viewable (and one day settable)
  • Get-EffectiveAccess will show limiting CARs when a CAP is assigned (right now, CAPs should be taken into account, but it won’t show which CAR is limiting access)
  • Get-EffectiveAccess will allow you to add group/device claims

 

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.