Get full control of Windows Update

  • Context

I’ve been invited to a strange meeting where 2 companies were talking about Windows Updates. The software manufacturer wanted to patch the computers once a year while the maintenance provider wanted to be never ever disturbed by an update or its reboot during a full week of production. I told the software company that this bad practice is called “patch and pray”. I told the maintenance company that’s hard to achieve nowadays due to “Windows Update as a Service” and that I’ll have a look.

While I understand the business needs of these companies, it seems that the behavior described by Brian Krebs in his recent blog post has already negatively impacted their subconscious mind.
Please allow me to quote his post:

Most Microsoft Windows (ab)users probably welcome the monthly ritual of applying security updates about as much as they look forward to going to the dentist: It always seems like you were there just yesterday, and you never quite know how it’s all going to turn out.

[…]

Nevertheless, it is frustrating when being diligent about applying patches introduces so many unfixable problems that you’re forced to completely reinstall the OS and all of the programs that ride on top of it.

[…]

So, three words of advice. First off, don’t let Microsoft decide when to apply patches and reboot your computer.

[…]

Secondly, it doesn’t hurt to wait a few days to apply updates.

[…]

Finally, please have some kind of system for backing up your files before applying any updates.

I usually recommend the same as Brian Krebs to any user who’s not involved in patch management.

  • Solution

To regain control on Windows Update (a.k.a. WU), the following solution allows you to turn WU on and off on demand.

#Requires -Version 4.0
#Requires -RunAsAdministrator
Function Invoke-PostInstallNoWUDeployment {
[CmdletBinding()]
Param()
Begin {
$HT = @{ ErrorAction = 'Stop'}
$ConfigurationData = @{
AllNodes =
@(
@{
NodeName = 'localhost'
Services = @(
@{ Name = 'BITS' ; StartupType = 'Manual' ; BuiltInAccount = 'LocalSystem' ; State = 'Stopped' },
@{ Name = 'wuauserv' ; StartupType = 'Disabled'; BuiltInAccount = 'LocalSystem' ; State = 'Stopped' }
)
}
)
}
Configuration WXPostInstallControlledWUDSCConfig {
param
(
[string[]]$NodeName = 'localhost'
)
Import-DscResource -ModuleName 'PSDesiredStateConfiguration'
Node $NodeName
{
LocalConfigurationManager
{
ConfigurationMode = 'ApplyAndAutoCorrect'
ConfigurationModeFrequencyMins = 30
RefreshFrequencyMins = 30
RebootNodeIfNeeded = $false
}
#region services
foreach ($s in $Node.Services)
{
Service $s.Name
{
Name = $s.Name;
BuiltInAccount = $s.BuiltInAccount ;
StartupType = $s.StartupType ;
State = $s.State
# State = 'Running';
}
}
#endregion
#region DSC WU UX
# BranchReadinessLevel REG_DWORD 0x20
Registry BranchReadinessLevel
{
Key = 'HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\WindowsUpdate\UX\Settings'
ValueName = 'BranchReadinessLevel'
Ensure = 'Present'
ValueData = '32'
ValueType = 'Dword'
Force = $true
}
# "ActiveHoursEnd"=dword:00000017 (23)
Registry ActiveHoursEnd
{
Key = 'HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\WindowsUpdate\UX\Settings'
ValueName = 'ActiveHoursEnd'
Ensure = 'Present'
ValueData = '23'
ValueType = 'Dword'
Force = $true
}
# "ActiveHoursStart"=dword:00000006 (6)
Registry ActiveHoursStart
{
Key = 'HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\WindowsUpdate\UX\Settings'
ValueName = 'ActiveHoursStart'
Ensure = 'Present'
ValueData = '6'
ValueType = 'Dword'
Force = $true
}
# "DeferFeatureUpdatesPeriodInDays"=dword:000000b9 (185d)
Registry DeferFeatureUpdatesPeriodInDays
{
Key = 'HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\WindowsUpdate\UX\Settings'
ValueName = 'DeferFeatureUpdatesPeriodInDays'
Ensure = 'Present'
ValueData = '185'
ValueType = 'Dword'
Force = $true
}
# "DeferQualityUpdatesPeriodInDays"=dword:00000019 (25d)
Registry DeferQualityUpdatesPeriodInDays
{
Key = 'HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\WindowsUpdate\UX\Settings'
ValueName = 'DeferQualityUpdatesPeriodInDays'
Ensure = 'Present'
ValueData = '25'
ValueType = 'Dword'
Force = $true
}
#endregion
#region WU Policies
Registry AUPowerManagement
{
Key = 'HKEY_LOCAL_MACHINE\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate'
ValueName = 'AUPowerManagement'
Ensure = 'Present'
ValueData = '0'
ValueType = 'Dword'
Force = $true
}
# SetActiveHours
Registry SetActiveHours
{
Key = 'HKEY_LOCAL_MACHINE\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate'
ValueName = 'SetActiveHours'
Ensure = 'Present'
ValueData = '1'
ValueType = 'Dword'
Force = $true
}
Registry ActiveHoursStartWU
{
Key = 'HKEY_LOCAL_MACHINE\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate'
ValueName = 'ActiveHoursStart'
Ensure = 'Present'
ValueData = '6'
ValueType = 'Dword'
Force = $true
}
Registry ActiveHoursEndWU
{
Key = 'HKEY_LOCAL_MACHINE\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate'
ValueName = 'ActiveHoursEnd'
Ensure = 'Present'
ValueData = '23'
ValueType = 'Dword'
Force = $true
}
#endregion
Script RemoveBOM {
GetScript = {
@{
GetScript = $GetScript
SetScript = $SetScript
TestScript = $TestScript
Result = ($true)
}
}
SetScript = {
# Remove BOM because File DSC resource creates a UTF8 file with BOM
'On','Off' |
Foreach-Object {
[System.IO.File]::WriteAllLines(
"C:\Users\Public\Desktop\$($_).cmd",
(Get-Content -Path "C:\Users\Public\Desktop\$($_).cmd"),
(New-Object System.Text.UTF8Encoding($False))
)
}
}
TestScript = {
return $false
}
DependsOn = '[File]WUOnCmd','[File]WUOffCmd'
}
File WUOnCmd {
DestinationPath = 'C:\Users\Public\Desktop\On.cmd'
Ensure = 'Present';
Force = $true
Contents = @'
@echo off
%systemroot%\system32\WindowsPowerShell\v1.0\powershell.exe -ExecutionPolicy Bypass "%systemroot%\WUOn.ps1"
pause
'@
}
File WUOffCmd {
DestinationPath = 'C:\Users\Public\Desktop\Off.cmd'
Ensure = 'Present';
Force = $true
Contents = @'
@echo off
%systemroot%\system32\WindowsPowerShell\v1.0\powershell.exe -ExecutionPolicy Bypass "%systemroot%\WUOff.ps1"
pause
'@
}
File WUOn {
DestinationPath = 'C:\windows\WUOn.ps1'
Ensure = 'Present';
Force = $true
Contents = @'
#Requires -RunAsAdministrator
reg.exe --% delete "HKEY_LOCAL_MACHINE\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate" /v "DoNotConnectToWindowsUpdateInternetLocations" /f /reg:64
reg.exe --% delete "HKEY_LOCAL_MACHINE\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate" /v "DisableDualScan" /f /reg:64
reg.exe --% delete "HKEY_LOCAL_MACHINE\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate\AU" /v "NoAutoUpdate" /f /reg:64
reg.exe --% delete "HKEY_LOCAL_MACHINE\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate\AU" /v "UseWUServer" /f /reg:64
reg.exe --% delete "HKEY_LOCAL_MACHINE\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate" /v "AllowAutoWindowsUpdateDownloadOverMeteredNetwork" /f /reg:64
reg.exe --% delete "HKEY_LOCAL_MACHINE\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate" /v "WUServer" /f /reg:64
reg.exe --% delete "HKEY_LOCAL_MACHINE\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate" /v "WUStatusServer" /f /reg:64
reg.exe --% delete "HKEY_LOCAL_MACHINE\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate" /v "UpdateServiceUrlAlternate" /f /reg:64
Get-NetAdapter |
Get-NetConnectionProfile |
ForEach-Object {
& (Get-Command -Name "$($env:systemroot)\system32\reg.exe") @(
'add',"HKLM\SOFTWARE\Microsoft\DusmSvc\Profiles\$($_.InstanceID)\*",'/v','UserCost','/t','REG_DWORD','/d','0x0','/f','/reg:64'
)
}
Restart-Service -Name 'DusmSvc' -Force -Verbose
Set-Service -Name 'wuauserv' -StartupType 'Automatic' -Verbose -Confirm:$false
Start-Service -Name 'wuauserv' -PassThru -Verbose -Confirm:$false
gpupdate.exe /force /target:computer
usoclient.exe RefreshSettings
'@
}
File WUOff {
DestinationPath = 'C:\windows\WUOff.ps1'
Ensure = 'Present';
Force = $true
Contents = @'
#Requires -RunAsAdministrator
reg.exe --% add "HKEY_LOCAL_MACHINE\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate" /v "DoNotConnectToWindowsUpdateInternetLocations" /t REG_dword /d 0x1 /f /reg:64
reg.exe --% add "HKEY_LOCAL_MACHINE\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate" /v "DisableDualScan" /t REG_dword /d 0x1 /f /reg:64
reg.exe --% add "HKEY_LOCAL_MACHINE\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate\AU" /v "NoAutoUpdate" /t REG_dword /d 0x1 /f /reg:64
reg.exe --% add "HKEY_LOCAL_MACHINE\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate\AU" /v "UseWUServer" /t REG_dword /d 0x1 /f /reg:64
reg.exe --% add "HKEY_LOCAL_MACHINE\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate" /v "AllowAutoWindowsUpdateDownloadOverMeteredNetwork" /t REG_dword /d 0x0 /f /reg:64
reg.exe --% add "HKEY_LOCAL_MACHINE\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate" /v "WUServer" /t REG_SZ /d "https://127.0.0.1:8531" /f /reg:64
reg.exe --% add "HKEY_LOCAL_MACHINE\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate" /v "WUStatusServer" /t REG_SZ /d "https://127.0.0.1:8531" /f /reg:64
reg.exe --% add "HKEY_LOCAL_MACHINE\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate" /v "UpdateServiceUrlAlternate" /t REG_SZ /d "" /f /reg:64
Get-NetAdapter |
Get-NetConnectionProfile |
ForEach-Object {
& (Get-Command -Name "$($env:systemroot)\system32\reg.exe") @(
'add',"HKLM\SOFTWARE\Microsoft\DusmSvc\Profiles\$($_.InstanceID)\*",'/v','UserCost','/t','REG_DWORD','/d','0x2','/f','/reg:64'
)
}
Restart-Service -Name 'DusmSvc' -Force -Verbose
Stop-Service -Name 'wuauserv' -Force -PassThru -Verbose -Confirm:$false
Set-Service -Name 'wuauserv' -StartupType 'Disabled' -Verbose -Confirm:$false
gpupdate.exe /force /target:computer
usoclient.exe RefreshSettings
'@
}
}
}
}
Process {
# Configure minimal WSMan/WinRM for DSC to work on Windows
Write-Verbose -Message 'Configuring WinRM WSMan listener for DSC'
Stop-Service -Name WinRM -PassThru |
Set-Service -StartupType Automatic -PassThru |
Start-Service
Get-NetFirewallRule -Name @(
'WINRM-HTTP-In-TCP', # Pubic
'WINRM-HTTP-In-TCP-NoScope') | #Domain,Private
Enable-NetFirewallRule -PassThru |
Get-NetFirewallAddressFilter |
Set-NetFirewallAddressFilter -RemoteAddress '127.0.0.1'
# Disable-NetFirewallRule -Name WINRM-HTTP-In-TCP-NoScope
Get-ChildItem -Path WSMan:\localhost\Listener -Include listener* |
Remove-Item -Recurse
New-WSManInstance winrm/config/Listener -SelectorSet @{Address="*";Transport="http"}
Set-Item -Path WSMan:\localhost\Service\Auth\Kerberos -Value $false -Force
Set-Item -Path WSMan:\localhost\Service\Auth\Negotiate -Value $true -Force
# Prepare for DSC
Write-Verbose -Message 'Starting to apply DSC configuration'
if (-not(test-path -Path "$($env:systemroot)\TEMP\DSC" -PathType Container)){
mkdir -Path "$($env:systemroot)\TEMP\DSC" -Force
}
# Compile DSC config
WXPostInstallControlledWUDSCConfig -OutputPath "$($env:systemroot)\TEMP\DSC" -ConfigurationData $ConfigurationData
# Apply it
Start-DscConfiguration -Path "$($env:systemroot)\TEMP\DSC" -ComputerName localhost -Verbose -Force -Wait
}
End {
'BITS','wuauserv' |
ForEach-Object {
Set-Service -Name $_ -StartupType Automatic -PassThru -Verbose
Start-Service -Name $_
}
# Remove next comment to turn off by default
# & 'C:\windows\WUOff.ps1'
}
} #endof Invoke-PostInstallNoWUDeployment
Invoke-PostInstallNoWUDeployment -Verbose >>C:\windows\temp\postinstall.log 3>&1 4>&1
view raw DSCNoWU.ps1 hosted with ❤ by GitHub

All the screenshots below have been done on a Windows 10 Enterprise 1903 provisioned in Azure:

  • How to install

Right-click the Start menu, select “Windows PowerShell (Admin)”,
Copy/paste in your browser the link to the above gist or click on it. Once you’re on the gist, you should use the ‘Raw’ button and then copy the content of the page and paste it in a PowerShell script file. I used a.ps1 as a name below:

Set-ExecutionPolicy -ExecutionPolicy Bypass -Scope Process -Force
notepad a.ps1
.\a.ps1

  • Demo

Let’s see what it looks like once it’s deployed.
You get 2 scripts on the desktop and the following settings in the system settings tiles:
The active hours are set to 6:00 AM to 11:00 PM.

It also defines a period of 185 days to defer upgrades and 25 days to defer updates. If that’s not what you want, you’ll need to modify the content of the script but that’s beyond the scope of this blog post.

If you use the Off.cmd script on the desktop, checking for updates fails (that’s expected)

If you use the On.cmd script on the desktop and hit “Retry”, checking for updates succeeds:

  • Known limits

The above solution doesn’t work in a corporate environment where the above WU settings are controlled by an administrator.
The above solution works only for an Ethernet connection because it’s set as a metered connection. That will also stop Office 365 updates (it honors the metered flag). Unfortunately the above solution isn’t working for a wireless connection.

How to delete a single Applocker rule

  • Context:

A colleague of mine was working on Applocker rules and the installer he was working on was so badly designed that we thought it would easier to restore the worst and most dangerous default rule named “*” for the built-in administrators.

If you restore this evil rule, you cannot and should not leave it there. You also have to delete it after it has been used to install successfully that ugly software.

  • Problem:

I started looking around using my google fu but only found the following web page from Microsoft named delete-an-applocker-rule that tells you actually how to clear *all* the rules. It’s a dead end and really not what we want 😦

  • Solution:

Instead I wrote the following function. Before looking at its code, let me tell you the following:

If you specify the first parameter to indicate what type of rules you want to delete, a Exe, Script, Msi, Appx, Dll, it will be used in the dynamic parameters block to enumerate all rules names found for this type.

Let’s see this in action:

You may also notice in the body of the function that the XML representation of Applocker rules never touches the disk. The rule is removed from the XML and its result is directly merged/reimported.

The function specifies a ‘high’ ConfirmImpact and SupportsShouldProcess because it’s destructive. There’s no backup of the rules or the rule being removed.

Last but not least, the rule is deleted but the group policies (GPO) are not refreshed. You’ll see its impact once the GPO have been reapplied.

Here’s the function:

#Requires -RunAsAdministrator
#Requires -Version 3.0
#Requires -PSEdition Desktop
Function Remove-LocalApplockerPolicyRule {
[CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'High')]
Param(
[ValidateSet('Exe','Script','Msi','Appx','Dll')]
[Parameter(Mandatory)]
[String]$Type
)
DynamicParam {
$Dictionary = New-Object -TypeName System.Management.Automation.RuntimeDefinedParameterDictionary
#region helper function
Function New-ParameterAttributCollection {
[CmdletBinding()]
Param(
[Switch]$Mandatory,
[Switch]$ValueFromPipeline,
[Switch]$ValueFromPipelineByPropertyName,
[String]$ParameterSetName,
[Parameter()]
[ValidateSet(
'Arguments','Count','Drive','EnumeratedArguments','Length','NotNull',
'NotNullOrEmpty','Pattern','Range','Script','Set','UserDrive'
)][string]$ValidateType,
[Parameter()]
$ValidationContent
)
Begin {
}Process {
$c = New-Object System.Collections.ObjectModel.Collection[System.Attribute]
$a = New-Object System.Management.Automation.ParameterAttribute
if ($Mandatory) {
$a.Mandatory = $true
}
if ($ValueFromPipeline) {
$a.ValueFromPipeline = $true
}
if ($ValueFromPipelineByPropertyName) {
$a.ValueFromPipelineByPropertyName=$true
}
if ($ParameterSetName) {
$a.ParameterSetName = $ParameterSetName
}
$c.Add($a)
if ($ValidateType -and $ValidationContent) {
try {
$c.Add((New-Object "System.Management.Automation.Validate$($ValidateType)Attribute"(
$ValidationContent
)))
} catch {
Throw $_
}
}
$c
}
End {}
}
#endregion
try {
$LocalApplockerPolicyXml = [xml](Get-AppLockerPolicy -Local -Xml -ErrorAction Stop)
} catch {
Throw 'Failed to read the local Applocker policy into XML'
}
#region param Rule
$Dictionary.Add(
'Rule',
(New-Object System.Management.Automation.RuntimeDefinedParameter(
'Rule',
[string],
(New-ParameterAttributCollection -Mandatory -ValidateType Set -ValidationContent (
$LocalApplockerPolicyXml.SelectNodes("/AppLockerPolicy/RuleCollection[@Type='$($PSBoundParameters['Type'])']").ChildNodes| ForEach-Object { $_.Name }
))
))
)
$Dictionary
}
Begin {
}
Process {
Write-Verbose -Message "Dealing with Rule Collection type: $($PSBoundParameters['Type'])"
Write-Verbose -Message "Dealing with Rule Name: $($PSBoundParameters['Rule'])"
# Select node
$n = $LocalApplockerPolicyXml.SelectNodes("/AppLockerPolicy/RuleCollection[@Type='$($PSBoundParameters['Type'])']").ChildNodes |
Where { $_.Name -eq "$($PSBoundParameters['Rule'])" }
if ($pscmdlet.ShouldProcess("$($n.OuterXml)", 'Remove rule')) {
try {
# Remove rule from xml
$null = $LocalApplockerPolicyXml.SelectNodes("/AppLockerPolicy/RuleCollection[@Type='$($PSBoundParameters['Type'])']").RemoveChild($n)
# Re-apply/import all rules except the removed rule
[Microsoft.Security.ApplicationId.PolicyManagement.PolicyModel.AppLockerPolicy]::FromXml($LocalApplockerPolicyXml.outerXML) |
Set-AppLockerPolicy -ErrorAction Stop
Write-Verbose -Message 'Successfully removed rule, a group policies refresh is required to see the impact of the removed rule'
} catch {
Throw "Something went wrong while trying to remove the applocker rule: $($_.Exception.Message)"
}
}
}
End {
}
} # endof Remove-LocalApplockerPolicyRule
Export-ModuleMember -Function 'Remove-LocalApplockerPolicyRule'