Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
113 changes: 113 additions & 0 deletions src/System.Management.Automation/security/wldpNativeMethods.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@
using System.Management.Automation.Tracing;
using System.Runtime.InteropServices;

using Microsoft.Win32;

namespace System.Management.Automation.Security
{
/// <summary>
Expand Down Expand Up @@ -466,6 +468,110 @@ private static SystemEnforcementMode GetWldpPolicy(string path, SafeHandle handl
private const string AppLockerTestFileName = "__PSScriptPolicyTest_";
private const string AppLockerTestFileContents = "# PowerShell test file to determine AppLocker lockdown mode ";

private const string AppLockerScriptPolicyKey = @"SOFTWARE\Policies\Microsoft\Windows\SrpV2\Script";
private const string SaferCodeIdentifiersKey = @"SOFTWARE\Policies\Microsoft\Windows\Safer\CodeIdentifiers";

// Windows can create the CodeIdentifiers key with only this value when no Software Restriction Policy is defined.
private const string SaferAuthenticodeEnabledValue = "AuthenticodeEnabled";

/// <summary>
/// Checks whether an AppLocker script policy or a Software Restriction Policy may be in effect.
/// Returns true when any of the locations exists or cannot be read.
/// </summary>
/// <returns>False only when no script policy is configured on the machine.</returns>
internal static bool HasAppLockerOrSaferScriptPolicy()
{
// A 32-bit process on 64-bit Windows sees a redirected system directory, so it cannot check the
// compiled policy file reliably. Keep the probe in that case.
if (Environment.Is64BitOperatingSystem && !Environment.Is64BitProcess)
{
return true;
}

// AppLocker compiles the effective script rules (group policy, local policy or MDM) into this file.
string compiledScriptPolicy = IO.Path.Combine(Environment.SystemDirectory, "AppLocker", "Script.AppLocker");
Comment thread
azchohfi marked this conversation as resolved.
Resolved

return PolicyFileMayExist(compiledScriptPolicy)
|| RegistryKeyMayExist(Registry.LocalMachine, AppLockerScriptPolicyKey)
|| !IsPolicyKeyEmpty(Registry.LocalMachine, SaferCodeIdentifiersKey, SaferAuthenticodeEnabledValue)
|| !IsPolicyKeyEmpty(Registry.CurrentUser, SaferCodeIdentifiersKey, SaferAuthenticodeEnabledValue);
}

/// <summary>
/// Returns false only if the file or its directory does not exist.
/// </summary>
internal static bool PolicyFileMayExist(string path)
{
try
{
IO.File.GetAttributes(path);
return true;
}
catch (IO.FileNotFoundException)
{
return false;
}
catch (IO.DirectoryNotFoundException)
{
return false;
}
catch (Exception e) when (e is IO.IOException or UnauthorizedAccessException)
{
return true;
}
}

/// <summary>
/// Returns false only if the registry key does not exist.
/// </summary>
internal static bool RegistryKeyMayExist(RegistryKey root, string subKeyPath)
{
try
{
using RegistryKey key = root.OpenSubKey(subKeyPath);
return key is not null;
}
catch (Exception e) when (e is System.Security.SecurityException or UnauthorizedAccessException or IO.IOException)
{
return true;
}
}

/// <summary>
/// Returns true if the registry key does not exist, or has no subkeys and no values other than
/// <paramref name="ignoredValueName"/>. Returns false if the key cannot be read.
/// </summary>
internal static bool IsPolicyKeyEmpty(RegistryKey root, string subKeyPath, string ignoredValueName)
{
try
{
using RegistryKey key = root.OpenSubKey(subKeyPath);
if (key is null)
{
return true;
}

if (key.SubKeyCount > 0)
{
return false;
}

foreach (string valueName in key.GetValueNames())
{
if (!string.Equals(valueName, ignoredValueName, StringComparison.OrdinalIgnoreCase))
{
return false;
}
}

return true;
}
catch (Exception e) when (e is System.Security.SecurityException or UnauthorizedAccessException or IO.IOException)
{
return false;
}
}

private static SystemEnforcementMode GetAppLockerPolicy(string path, SafeHandle handle)
{
SaferPolicy result = SaferPolicy.Disallowed;
Expand All @@ -480,6 +586,13 @@ private static SystemEnforcementMode GetAppLockerPolicy(string path, SafeHandle
{
result = s_cachedSaferSystemPolicy.Value;
}
else if (!HasAppLockerOrSaferScriptPolicy())
{
// Neither AppLocker nor Software Restriction Policies have a script policy on this machine,
// so the test files below would be allowed. Skip writing and checking them.
result = SaferPolicy.Allowed;
s_cachedSaferSystemPolicy = result;
}
else
{
//
Expand Down
53 changes: 53 additions & 0 deletions test/powershell/engine/Security/AppLockerProbe.Tests.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT License.

Describe "AppLocker policy test files at startup" -Tags 'CI' {
BeforeAll {
$skipTest = -not $IsWindows
if (-not $skipTest) {
Import-Module HelpersCommon

$systemPolicy = [System.Management.Automation.Security.SystemPolicy]
$flags = [System.Reflection.BindingFlags]'NonPublic, Static'
$hasScriptPolicy = $systemPolicy.GetMethod('HasAppLockerOrSaferScriptPolicy', $flags).Invoke($null, $null)

# When WDAC enforces lockdown, the AppLocker check is never reached.
$null = $systemPolicy::GetSystemLockdownPolicy()
$wldpPolicy = $systemPolicy.GetField('s_cachedWldpSystemPolicy', $flags).GetValue($null)
$expectTestFiles = $hasScriptPolicy -and ($wldpPolicy -ne [System.Management.Automation.Security.SystemEnforcementMode]::Enforce)
}
}

It "Writes __PSScriptPolicyTest files only when a script policy may exist" -Skip:$skipTest {
$tempDir = Join-Path $TestDrive 'temp'
$null = New-Item -ItemType Directory -Path $tempDir
$watcher = [System.IO.FileSystemWatcher]::new($tempDir, '__PSScriptPolicyTest_*')
$sourceId = 'AppLockerProbe_' + [guid]::NewGuid()
$null = Register-ObjectEvent -InputObject $watcher -EventName Created -SourceIdentifier $sourceId
$watcher.EnableRaisingEvents = $true

$oldTemp = $env:TEMP
$oldTmp = $env:TMP
try {
$env:TEMP = $tempDir
$env:TMP = $tempDir
Start-NativeExecution { & "$PSHOME/pwsh" -NoProfile -NonInteractive -Command exit }
Start-Sleep -Milliseconds 500
$created = @(Get-Event -SourceIdentifier $sourceId -ErrorAction Ignore)
}
finally {
$env:TEMP = $oldTemp
$env:TMP = $oldTmp
Unregister-Event -SourceIdentifier $sourceId
Remove-Event -SourceIdentifier $sourceId -ErrorAction Ignore
$watcher.Dispose()
}

if ($expectTestFiles) {
$created.Count | Should -BeGreaterThan 0
}
else {
$created.Count | Should -Be 0
}
}
}
108 changes: 108 additions & 0 deletions test/xUnit/csharp/test_SystemPolicy.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

#if !UNIX
using System;
using System.IO;
using System.Management.Automation.Security;
using System.Runtime.Versioning;
using Microsoft.Win32;
using Xunit;

namespace PSTests.Parallel
{
[SupportedOSPlatform("windows")]
public static class SystemPolicyTests
{
[Fact]
public static void TestIsPolicyKeyEmpty()
{
string testKeyPath = @"Software\PowerShellTests\SystemPolicy_" + Guid.NewGuid().ToString("N");
string codeIdentifiers = testKeyPath + @"\CodeIdentifiers";
try
{
// Key does not exist.
Assert.True(SystemPolicy.IsPolicyKeyEmpty(Registry.CurrentUser, codeIdentifiers, "AuthenticodeEnabled"));

using (RegistryKey key = Registry.CurrentUser.CreateSubKey(codeIdentifiers))
{
// Key exists without values or subkeys.
Assert.True(SystemPolicy.IsPolicyKeyEmpty(Registry.CurrentUser, codeIdentifiers, "AuthenticodeEnabled"));

// Only the ignored value.
key.SetValue("authenticodeenabled", 0, RegistryValueKind.DWord);
Assert.True(SystemPolicy.IsPolicyKeyEmpty(Registry.CurrentUser, codeIdentifiers, "AuthenticodeEnabled"));
Assert.False(SystemPolicy.IsPolicyKeyEmpty(Registry.CurrentUser, codeIdentifiers, ignoredValueName: null));

// Any other value means a policy is configured.
key.SetValue("DefaultLevel", 0x40000, RegistryValueKind.DWord);
Assert.False(SystemPolicy.IsPolicyKeyEmpty(Registry.CurrentUser, codeIdentifiers, "AuthenticodeEnabled"));
key.DeleteValue("DefaultLevel");

// So does any subkey (rules).
key.CreateSubKey(@"262144\Paths").Dispose();
Assert.False(SystemPolicy.IsPolicyKeyEmpty(Registry.CurrentUser, codeIdentifiers, "AuthenticodeEnabled"));
}
}
finally
{
Registry.CurrentUser.DeleteSubKeyTree(testKeyPath, throwOnMissingSubKey: false);
}
}

[Fact]
public static void TestPolicyFileMayExist()
{
string directory = Path.Combine(Path.GetTempPath(), "SystemPolicy_" + Guid.NewGuid().ToString("N"));
string file = Path.Combine(directory, "Script.AppLocker");

// Directory does not exist.
Assert.False(SystemPolicy.PolicyFileMayExist(file));

Directory.CreateDirectory(directory);
try
{
Assert.False(SystemPolicy.PolicyFileMayExist(file));

File.WriteAllText(file, string.Empty);
Assert.True(SystemPolicy.PolicyFileMayExist(file));
}
finally
{
Directory.Delete(directory, recursive: true);
}
}

[Fact]
public static void TestRegistryKeyMayExist()
{
string testKeyPath = @"Software\PowerShellTests\SystemPolicy_" + Guid.NewGuid().ToString("N");
string script = testKeyPath + @"\SrpV2\Script";
try
{
Assert.False(SystemPolicy.RegistryKeyMayExist(Registry.CurrentUser, script));

// An existing AppLocker key counts even when it is empty.
Registry.CurrentUser.CreateSubKey(script).Dispose();
Assert.True(SystemPolicy.RegistryKeyMayExist(Registry.CurrentUser, script));
}
finally
{
Registry.CurrentUser.DeleteSubKeyTree(testKeyPath, throwOnMissingSubKey: false);
}
}

[Fact]
public static void TestHasAppLockerOrSaferScriptPolicyMatchesMachine()
{
bool expected = (Environment.Is64BitOperatingSystem && !Environment.Is64BitProcess)
|| SystemPolicy.PolicyFileMayExist(Path.Combine(Environment.SystemDirectory, "AppLocker", "Script.AppLocker"))
|| SystemPolicy.RegistryKeyMayExist(Registry.LocalMachine, @"SOFTWARE\Policies\Microsoft\Windows\SrpV2\Script")
|| !SystemPolicy.IsPolicyKeyEmpty(Registry.LocalMachine, @"SOFTWARE\Policies\Microsoft\Windows\Safer\CodeIdentifiers", "AuthenticodeEnabled")
|| !SystemPolicy.IsPolicyKeyEmpty(Registry.CurrentUser, @"SOFTWARE\Policies\Microsoft\Windows\Safer\CodeIdentifiers", "AuthenticodeEnabled");

Assert.Equal(expected, SystemPolicy.HasAppLockerOrSaferScriptPolicy());
}
}
}
#endif