Defense in Depth: Hardening clients against malware deployment by disabling unused Group Policy CSEs

This is an idea that came off the back of a customer being hit with ransomware. The deployment method used by the attackers was simple: They got domain admin and then proceeded to create a GPO that deployed the ransomware over a weekend using Group Policy Software installation. By Monday morning, over 10k clients and servers had been hit.

Many organizations don’t really use Group Policy Software installation. In fact, I ran a Twitter poll and over 80% of people that responded said they didn’t use it and those that did used it for small tasks that could potentially be done another way. Even some Group Policy MVPs didn’t recommend it. See Twitter poll here https://twitter.com/colinford/status/1539046821510623234.

So if we are not using it, can we just disable it to reduce the attack surface? Now I’m not going to talk about application whitelisting, which yes you should be doing but this too can have holes in it if not implemented / managed correctly. I’m talking about defense in depth, adding another layer of hardening to the client. As I have not seen this topic discussed in security guidance before I thought it would be interesting to explore.

First, what is a Group Policy CSE?

Group Policy processing on a client is handled by multiple Client-Side Extensions (CSEs). For example Folder Redirection, Script Processing, Drive Mapping are all separate CSEs. These are represented in the registry as a list of GUIDs, which in turn have a bunch of registry options to specify how the CSE should process group policy i.e. which DLL to use that knows how to handle the processing, should user settings be processed, should it run during background group policy processing and so on.

The list of CSEs can be found on a client here:

HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon\GPExtensions

So how do we disable one?

Attempting to Disable the Software installation CSE

Disclaimer: Before reading further this is not documented by Microsoft, even though it works. You should seek Microsoft guidance and always test in a development environment first.

Now that that is out the way, the location of the Software installation CSE in the registry is here:

HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon\GPExtensions\{c6dc5466-785a-11d2-84d0-00c04fb169f7}

As I mentioned, there are a bunch of registry keys that determine how a CSE should process policies. All of these registry keys are documented here:

https://docs.microsoft.com/en-us/previous-versions/windows/desktop/policy/creating-a-policy-callback-function

The keys of interest are:

  • NoBackgroundPolicy
  • NoUserPolicy
  • NoMachinePolicy

By creating these keys (if missing) and setting to (DWORD) 0x1 the CSE should not process any policies at all on the client. Setting these keys is not super straight forward due to their default ACL. Only the special user TrustedInstaller can modify them, even SYSTEM has no access. In order to set these you need to perform the following actions while running as SYSTEM:

  1. Take ownership of the CSE’s GUID registry key
  2. Give Full Control permission to SYSTEM
  3. Set the registry keys
  4. Remove Full Control permission from SYSTEM
  5. Set owner back to NT SERVICE\TrustedInstaller

See the script at end of post for an automated version.

Conclusion

After testing this in a lab, it did indeed did disable Software installation processing without any visible issues on the client or within Event Log. The keys would likely need to be re-applied during a Windows upgrade; ConfigMgr Compliance Settings come to mind.

There are other CSEs I can think of that would be worth disabling for this purpose as well e.g. Scripts and Group Policy Scheduled Tasks. I think at least one of these is good to keep to use for common things like ConfigMgr client remediation. Personally, I would be OK with disabling Software Installation, even Scheduled Tasks and leaving one avenue open for remediation scripts like the Scripts CSE, but then really lock this down tight.

All in all, yes if you have domain admin you can get around this if you knew it was disabled, but you could potentially use your favorite security product to harden these registry keys after they are set.

Do you think this is worth including in client hardening discussions? Please leave a comment on Twitter. https://twitter.com/colinford/status/1540214785668546560

#-----------------------------------------------------
# Set registry keys
# 24/06/2022
# Twitter: @colinford
#
# Will elevate as necessary. Run as SYSTEM if you need to modify GPExtensions keys
#-----------------------------------------------------

# HKLM registry key to configure. See function "SetKeys" for which keys to set
$g_regPathHKLM = 'SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon\GPExtensions\{c6dc5466-785a-11d2-84d0-00c04fb169f7}'

#-----------------------------
# Main script block
#-----------------------------
$Main={

    # Get ACL object for registry path and copy current owner and ACL so we can restore it later
    #
    $g_currentUser = [System.Security.Principal.WindowsIdentity]::GetCurrent().Name
    $g_acl = Get-Acl "HKLM:\$g_regPathHKLM"
    $g_rules = $g_acl.GetAccessRules( $true, $true, [security.principal.ntaccount] )
    $g_currentOwner = $g_acl.Owner

    # Display variables
    Write-Host "-------------------------------------"
    Write-Host "Set Registry Keys"
    Write-Host "-------------------------------------"
    Write-Host "Reg path:     " "HKLM:\$g_regPathHKLM"
    Write-Host "Reg owner:    " $g_currentOwner
    Write-Host "Current user: " $g_currentUser
    Write-Host ""

    # Try set reg keys as current user first
    #
    Write-Host "Trying to set reg keys as current user"

    try {
        SetKeys "HKLM:\$g_regPathHKLM"
        Exit 0
    } catch {
        Write-Host "Could not set keys, trying to grant permission to current user"
    }

    # Elevate script process permissions required to take ownership and set permission
    #
    Write-Host "Elevating process privileges"
    enable-privilege SeTakeOwnershipPrivilege
    enable-privilege SeBackupPrivilege
    enable-privilege SeRestorePrivilege

    # Try to take ownership of key
    #
    Write-Host "Setting ownership to:" $g_currentUser

    try {
        setRegHKLMOwn $g_regPathHKLM $g_currentUser

    } catch {
        Write-Host "Could not set owner, exiting with failure 1"
        Exit 1
    }

    # Attempt to set full control permission on key
    #
    Write-Host "Setting full control permission for:" $g_currentUser

    try {
        setRegFullControl $g_regPathHKLM $g_currentUser
    } catch {
        Write-Host "Could not set permissions"
    
        # If failed, back out and reset ownership back to original owner
        #
        Write-Host "Setting ownership back to:" $g_currentOwner

        try {
            setRegHKLMOwn $g_regPathHKLM $g_currentOwner
        } catch {
            Write-Host "Could not set original owner, exiting with failure 3"
            Exit 3
        }
    Write-Host "Exiting with failure 2"
    Exit 2
    }

    # Set reg key values
    #
    Write-Host ""
    Write-Host "Setting registry keys"
    Write-Host "---------------------"
    try {
        SetKeys "HKLM:\$g_regPathHKLM"
    } catch {
        Write-Host "Could not set reg keys"
    }

    # Restore permissions
    #
    Write-Host "Resetting permissions on registry key"
    try {
        $g_acl.RemoveAccessRuleAll( $g_rules[0] )
        $g_acl.AddAccessRule( $g_rules[0] )
        set-acl -path "HKLM:\$g_regPathHKLM" -aclobject $g_acl
    } catch {
        Write-Host "Error removing permissions"
    }

    # Restore owner
    #
    Write-Host "Resetting ownership back to:" $g_currentOwner

    try {
        setRegHKLMOwn $g_regPathHKLM $g_currentOwner
    } catch {
        Write-Host "Could not set owner back, exiting with failure 3"
        Exit 3
    }

    Write-Host "Success!"
}

#-----------------------------
# Set the registry keys
#-----------------------------
Function SetKeys($regPath)
{
    New-ItemProperty -Path $regPath -Name "NoBackgroundPolicy" -Value 1 -PropertyType DWORD -Force -ErrorAction Stop
    New-ItemProperty -Path $regPath -Name "NoMachinePolicy" -Value 1 -PropertyType DWORD -Force -ErrorAction Stop
    New-ItemProperty -Path $regPath -Name "NoUserPolicy" -Value 1 -PropertyType DWORD -Force -ErrorAction Stop
}

#-----------------------------
# Set registry key owner
#-----------------------------
Function setRegHKLMOwn($regPathHKLM, $Owner)
{
    $acl = Get-Acl "HKLM:\$regPathHKLM"
    $key = [Microsoft.Win32.Registry]::LocalMachine.OpenSubKey($regPathHKLM,[Microsoft.Win32.RegistryKeyPermissionCheck]::ReadWriteSubTree,[System.Security.AccessControl.RegistryRights]::TakeOwnership)
    $idRef = [System.Security.Principal.NTAccount]($Owner)
    $acl.SetOwner($idRef)
    $key.SetAccessControl($acl)   
}

#-----------------------------
# Set registry key full control permission for a user
#-----------------------------
Function setRegFullControl($regPathHKLM, $User)
{
    $key = [Microsoft.Win32.Registry]::LocalMachine.OpenSubKey($regPathHKLM,[Microsoft.Win32.RegistryKeyPermissionCheck]::ReadWriteSubTree,[System.Security.AccessControl.RegistryRights]::ChangePermissions)
    $acl = $key.GetAccessControl()
    $rule = New-Object System.Security.AccessControl.RegistryAccessRule ($User,"FullControl","Allow")
    $acl.AddAccessRule($rule)
    $key.SetAccessControl($acl)
}

#-----------------------------
# Enable a specific privilege for the running script process
#
# Credit: Taken from https://social.technet.microsoft.com/Forums/Lync/en-US/e718a560-2908-4b91-ad42-d392e7f8f1ad/take-ownership-of-a-registry-key-and-change-permissions?forum=winserverpowershell
# http://twitter.com/toenuff
#-----------------------------
function enable-privilege {
 param(
  ## The privilege to adjust. This set is taken from
  ## http://msdn.microsoft.com/en-us/library/bb530716(VS.85).aspx
  [ValidateSet(
   "SeAssignPrimaryTokenPrivilege", "SeAuditPrivilege", "SeBackupPrivilege",
   "SeChangeNotifyPrivilege", "SeCreateGlobalPrivilege", "SeCreatePagefilePrivilege",
   "SeCreatePermanentPrivilege", "SeCreateSymbolicLinkPrivilege", "SeCreateTokenPrivilege",
   "SeDebugPrivilege", "SeEnableDelegationPrivilege", "SeImpersonatePrivilege", "SeIncreaseBasePriorityPrivilege",
   "SeIncreaseQuotaPrivilege", "SeIncreaseWorkingSetPrivilege", "SeLoadDriverPrivilege",
   "SeLockMemoryPrivilege", "SeMachineAccountPrivilege", "SeManageVolumePrivilege",
   "SeProfileSingleProcessPrivilege", "SeRelabelPrivilege", "SeRemoteShutdownPrivilege",
   "SeRestorePrivilege", "SeSecurityPrivilege", "SeShutdownPrivilege", "SeSyncAgentPrivilege",
   "SeSystemEnvironmentPrivilege", "SeSystemProfilePrivilege", "SeSystemtimePrivilege",
   "SeTakeOwnershipPrivilege", "SeTcbPrivilege", "SeTimeZonePrivilege", "SeTrustedCredManAccessPrivilege",
   "SeUndockPrivilege", "SeUnsolicitedInputPrivilege")]
  $Privilege,
  ## The process on which to adjust the privilege. Defaults to the current process.
  $ProcessId = $pid,
  ## Switch to disable the privilege, rather than enable it.
  [Switch] $Disable
 )

 ## Taken from P/Invoke.NET with minor adjustments.
 $definition = @'
 using System;
 using System.Runtime.InteropServices;
  
 public class AdjPriv
 {
  [DllImport("advapi32.dll", ExactSpelling = true, SetLastError = true)]
  internal static extern bool AdjustTokenPrivileges(IntPtr htok, bool disall,
   ref TokPriv1Luid newst, int len, IntPtr prev, IntPtr relen);
  
  [DllImport("advapi32.dll", ExactSpelling = true, SetLastError = true)]
  internal static extern bool OpenProcessToken(IntPtr h, int acc, ref IntPtr phtok);
  [DllImport("advapi32.dll", SetLastError = true)]
  internal static extern bool LookupPrivilegeValue(string host, string name, ref long pluid);
  [StructLayout(LayoutKind.Sequential, Pack = 1)]
  internal struct TokPriv1Luid
  {
   public int Count;
   public long Luid;
   public int Attr;
  }
  
  internal const int SE_PRIVILEGE_ENABLED = 0x00000002;
  internal const int SE_PRIVILEGE_DISABLED = 0x00000000;
  internal const int TOKEN_QUERY = 0x00000008;
  internal const int TOKEN_ADJUST_PRIVILEGES = 0x00000020;
  public static bool EnablePrivilege(long processHandle, string privilege, bool disable)
  {
   bool retVal;
   TokPriv1Luid tp;
   IntPtr hproc = new IntPtr(processHandle);
   IntPtr htok = IntPtr.Zero;
   retVal = OpenProcessToken(hproc, TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY, ref htok);
   tp.Count = 1;
   tp.Luid = 0;
   if(disable)
   {
    tp.Attr = SE_PRIVILEGE_DISABLED;
   }
   else
   {
    tp.Attr = SE_PRIVILEGE_ENABLED;
   }
   retVal = LookupPrivilegeValue(null, privilege, ref tp.Luid);
   retVal = AdjustTokenPrivileges(htok, false, ref tp, 0, IntPtr.Zero, IntPtr.Zero);
   return retVal;
  }
 }
'@

 $processHandle = (Get-Process -id $ProcessId).Handle
 $type = Add-Type $definition -PassThru
 $type[0]::EnablePrivilege($processHandle, $Privilege, $Disable)
}

& $Main

Leave a comment