Use PowerShell to Debug Slow GPO Processing

This blog provides an example of how we can use PowerShell to debug slow GPO processing.

I wrote it a couple of years ago when we needed to debug why some machines were taking so long to log in.  They were stuck on the “applying group policies” screen for far too long, so we wanted to generate a report that detailed how long each policy took to process.

This PowerShell script does exactly that.  It queries the event logs on remote machine(s) and writes a report to a local log file.  In this example you will need to change the $logPath and update the computer names in ("computer1","computer2").

cls

$runDateTime = (get-date -format dd-MM-yyyy_HH-mm-ss).ToString()
$logPath = "C:\Alkane\"

function Write-Log($logFile, $message) {

    write-host $message

    if (test-path $logPath) {        
        $message | Out-File -FilePath $logFile -Append
    } else {
        write-host "*Could not find path to write logs"
    }
}


function get-EventData($events) {
    $event = @($events)[@($events).count-1]
    if ($event -and $event.gettype().name -eq "EventLogRecord") {
        $nodes = ([xml]$event.toXML()).event.eventData.childnodes
        $hash = @{}; foreach($n in $nodes){$hash.$($n.name) = $n.'#text'}
        return new-object PsObject -Property $hash
    }
}


function Get-GPOSpeed{
    param([string]$computerName)

    $logFile = $logPath + $computerName + "_" + $runDateTime + ".log"
      
	Write-Log $logFile "Processing $computerName..."

	if (!(Test-Connection $computerName -Count 1 -Quiet)) {
		Write-Log $logFile "Cannot ping $computerName.  Skipping."
		return;
	}

	#find username of currently logged in user
	$explorer = Get-WmiObject -ComputerName $computerName -Class Win32_Process -Filter 'Name = "explorer.exe"' | select -First 1
	if ($explorer -eq $null) {
		Write-Log $logFile "There is no user logged in.  Script cannot continue."
		return;
	}
	
	$user = $explorer.GetOwner()
	$username = "$($user.Domain)\$($user.User)"

	Write-Log $logFile "Logged in user detected as $Username"

	# get the main logon event that includes the activity-Id:
	$log = "Microsoft-Windows-GroupPolicy/Operational"
	$xPath = "*[EventData[Data[@Name='PrincipalSamName'] and (Data='$Username')]] and *[System[(EventID='4001')]]"
	$logon = Get-WinEvent -ComputerName $computerName -logname $log -FilterXPath $xPath -MaxEvents 1 -ea 0

	if ($logon -eq $null) {
		Write-Log $logFile "Cannot find a login event (user keeps locking/unlocking as opposed to logging off/on?).  Script cannot continue."
		return;
	}

	$start = $logon.TimeCreated.ToUniversalTime().ToString('s')
	$id = $logon.ActivityId.Guid

	Write-Log $logFile "Activity filter for event viewer logs is $id"

	# get all logon-events related to this logon-process:
	$xPath = "*[System[TimeCreated[@SystemTime >= '$start'] and Correlation[@ActivityID='{$id}']]]"
	$logonEvents = Get-WinEvent -ComputerName $computerName -logname $log -FilterXPath $xPath -ea 0

	# completeUserLogonPolicy:
	$completeUserLogonPolicy = get-EventData $logonEvents.where({$_.id -eq 8001})
	$completeUserLogonPolicyDuration = $completeUserLogonPolicy.PolicyElaspedTimeInSeconds

	# get the exact logon duration:
	$logonEndData = get-EventData $logonEvents.where({$_.id -eq 5117})
	$duration = $logonEndData.SessionTimeElapsedInMilliseconds
	$duration = ([TimeSpan]::FromMilliseconds($duration)).TotalSeconds

	# discovery time for the closest domain controller:
	$dcSelectData = get-EventData $logonEvents.where({$_.id -eq 5326})
	$dcDiscoveryTime = $dcSelectData.DCDiscoveryTimeInMilliSeconds
	$dcDiscoveryTime = ([TimeSpan]::FromMilliseconds($dcDiscoveryTime)).TotalSeconds

	# domain controller details:
	$dcData = get-EventData $logonEvents.where({$_.id -eq 5308})
	$dcName = $dcData.DCName
	$dcIp = $dcData.DCIPAddress
	
	# network speed to DC:
	$dcNetData = get-EventData $logonEvents.where({$_.id -eq 5314})
	$dcSpeed = $dcNetData.BandwidthInkbps
	$isSlowLink = $dcNetData.IsSlowLink

	# logon breakdown by CSE-type:
	$cseEndEvents = $logonEvents.where({$_.id -eq 5016})
	$cseData = foreach($e in $cseEndEvents) {
	$data = get-EventData $e
		New-Object PsObject -Property @{
			time = $e.TimeCreated
			name = $data.CSEExtensionName
			duration = ([TimeSpan]::FromMilliseconds([int]$data.CSEElaspedTimeInMilliSeconds)).TotalSeconds
		}
	}

	# logon breakdown by GPOs:
	$gpoEndEvents = $logonEvents.where({$_.id -eq 4016})
	$gpoData = foreach($e in $gpoEndEvents) {
		$data = get-EventData $e
		$gpolistdata = get-EventData $data.ApplicableGPOList
		$gpoXml = [xml]"<GPOs>$($data.ApplicableGPOList)</GPOs>"
		$gpoList = $gpoXml.GPOs.GPO | select name, id -Unique
		$gpoList = $gpoList | sort name
		foreach ($g in $gpoList) {
			 New-Object PsObject -Property @{
				time = $e.TimeCreated
				ext = $data.CSEExtensionName
				gpo = $g.Name
			}
		}
	}

	# GPO network speed:
	$gpoNetData = get-EventData $logonEvents.where({$_.id -eq 5327})
	$gpoSpeed = $gpoNetData.NetworkBandwidthInKbps

	# list of all GPOs (use this to get the count for now, since this bit doesnt contain timings)
	$gpoListEvent = get-EventData $logonEvents.where({$_.id -eq 5312})
	$gpoXml = [xml]"<GPOs>$($gpoListEvent.GPOInfoList)</GPOs>"
	$gpoList = $gpoXml.GPOs.GPO | select name, id -Unique
	$gpoList = $gpoList | sort name
	$gpoCount = @($gpoList).count

	# number of error-events:
	$errCount = 0
	$errMessage = ""
	foreach($e in $logonEvents) {
		$data = get-EventData $e
		$errorCode = $data.ErrorCode
		$errorMess = $data.CSEExtensionName

		if ($errorCode -gt 0) {
			$errCount += 1
			$errMessage += "$($e.TimeCreated.ToString('dd/MM/yyyy HH:mm:ss')) - $errorMess - $(&net helpmsg $errorCode)"
		}
	}


	Write-Log $logFile "User logon data for $username is as follows:"
	Write-Log $logFile "Logon Duration: $completeUserLogonPolicyDuration seconds"
	Write-Log $logFile "Logon Time: $($logon.TimeCreated.ToString('dd/MM/yyyy HH:mm:ss'))"
	Write-Log $logFile "Exact Login Duration: $duration seconds"
	Write-Log $logFile "Error Count: $errCount"
	Write-Log $logFile "Error Text: $errMessage"
	Write-Log $logFile "DC Search Time: $dcDiscoveryTime seconds"
	Write-Log $logFile "DC Name: $dcName"
	Write-Log $logFile "DC IP Address: $dcIp"
	Write-Log $logFile "DC Connection Speed: $dcSpeed kbps"
	Write-Log $logFile "DC Slow Link: $isSlowLink"
	Write-Log $logFile "GPO Connection Speed: $gpoSpeed kbps"
	Write-Log $logFile "Unique GPO Count: $gpoCount"

	$cseData | sort time | foreach {
		$cseTime = $_.time
		$cseName = $_.name
		$cseDuration = "$($_.duration) seconds"

		Write-Log $logFile "$cseTime - $cseName - $cseDuration"
		$gpoData | sort time | foreach {
			$gpoTime = $_.time
			$gpoExt = $_.ext
			$gpoName = $_.gpo
			if ($cseName -eq $gpoExt) {
				Write-Log $logFile "    $gpoName"
			}
		}
	}    
}

#add computer names here
foreach ($computerName in  ("computer1","computer2")) {
    Get-GPOSpeed $computerName
}

Override Group Policy When Debugging Applications

Every now and then when I’m debugging an application in a production environment, I need to override various configurations such as proxies and browser security settings etc. so I can figure out what the heck is going on!

These settings are generally locked down via Group Policy.  So this blog aims to document a few registry tweaks that will enable us to override Group Policy when debugging applications.  Remember that these changes will only persist temporarily until Group Policy refreshes at the next interval, but they should provide us with a window of opportunity to debug our application!

Viewing the Registry of the Current User

Before we discuss any registry tweaks, it’s important (in the case of current user registry) to make sure you are viewing the correct registry data!

Typically, most organisations will disable regedit by default for standard users (“Registry editing has been disabled by your administrator”). And to run regedit you will no doubt have to run regedit under the context of another elevated user account.

What this means is that when you view the HKCU hive in the registry, it will be the current user registry of the elevated user account running regedit and not the logged in user account!

To circumvent this, we we need to find the security identifier (SID) of the current logged in user.  To do this we search through the subkeys in the following location to find the correct profile:

HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProfileList

Read the ProfileImagePath value under each subkey, and when we see the username of the logged in user, the SID for the user account is the name of the registry subkey itself and will typically start with ‘S-1-5-21’:

Now that we know the SID for the logged in user, we have 2 options.  The first option is to enable regedit for standard users.  This way you can then launch regedit as the logged in user (as opposed to running it as an elevated account), and the HKCU registry you see under HKCU will be for the current user.

Set this registry value, then close regedit and re-launch regedit as the logged in user.

HKEY_USERS\{SID}\Software\Microsoft\Windows\CurrentVersion\Policies\System 
DisableRegistryTools
REG_DWORD 0 

Option 2 is just to continue navigating HKEY_USERS to make changes to the current user registry from an elevated account.

Override Group Policy When Debugging Applications

You should consider that the following tweaks might ultimately be in different hives for your organisation (HKLM/HKCU) depending upon whether they have been set as a Computer configuration or a User configuration via Group Policy.  I also stumbled upon this resource which can be quite useful to find registry locations too.

Below we will list registry settings that can be used when we want to override Group Policy when debugging applications:

Restricted Apps (delete string values as required)

Sometimes when you launch an application (such as powershell.exe), nothing appears to happen.  No error.  But it doesn’t launch!  Chances are the executable is listed here:

HKCU\Software\Microsoft\Windows\CurrentVersion\Policies\Explorer\DisallowRun

Enable the Command Prompt

Ever seen the message ‘The command prompt has been disabled by your administrator’ message?  You can enable the command prompt by changing DisableCMD to 0:

HKCU\Software\Policies\Microsoft\Windows\System
DisableCMD
REG_DWORD 0

Restricted Control Panel applets (delete string values as required)

Similar to the above, sometimes you want to launch a control panel applet such as AppWiz.cpl, so we can view installed programs etc.  If it doesn’t launch, chances are it is listed in here:

HKCU\Software\Microsoft\Windows\CurrentVersion\Policies\Explorer\RestrictCpl

Enable TLS 1.0, 1.1 and 1.2 in browser

Sometimes we may be debugging an application that uses TLS 1.0.  Of course, this should really be disabled nowadays.  To temporarily debug it we can enable TLS 1.0 like so:

SSL/TLS Version Decimal Hexidecimal
SSL 2.0 8 0x8
SSL 3.0 32 0x20
TLS 1.0 128 0x80
TLS 1.1 512 0x200
TLS 1.2 2048 0x800

The below will enable TLS 1.0, TLS 2.0 and TLS 3.0.

HKCU\Software\Policies\Microsoft\Windows\CurrentVersion\Internet Settings
SecureProtocols REG_DWORD
2688

Enable ‘Automatic Configuration’ settings under ‘LAN Settings’ in Internet Explorer

You may wish to manually override the current proxy automatic configuration (PAC) file in local area network (LAN) settings.  If it is greyed out/disabled, this setting will enable it:

HKLM\SOFTWARE\Policies\Microsoft\Internet Explorer\Control Panel
Autoconfig REG_DWORD
0

Configure PAC File

We can also override the proxy PAC file directly in the registry:

HKCU\Software\Microsoft\Windows\CurrentVersion\Internet Settings
AutoConfigURL REG_SZ 
"https://yourproxy.pac"

And if we want to specify a proxy server directly, we can delete the value above and add:

HKCU\Software\Microsoft\Windows\CurrentVersion\Internet Settings
ProxyServer REG_SZ "alkaneproxy.local:8080"
ProxyEnable REG_DWORD 1
ProxyOverride REG_SZ <leave blank>