# Windows 10, Safe Mode, elevated. # SecurityHealthService is owned by TrustedInstaller. Do not add it. # webthreatdefsvc and webthreatdefusersvc are Windows 11 services. Do not add them. # Sense may be missing. Every other name must end at Start 4. # Exit 0 only when those Start values are 4 and every returned task is Disabled. # No tasks returned is a warning. Safe Mode often does not start Task Scheduler. # The final gate is Check_Defender_Disabled.PS1 after a normal boot. function Get-DefenderDisablePlan { @( @{ Name = 'WinDefend'; Optional = $false } @{ Name = 'Sense'; Optional = $true } @{ Name = 'WdNisSvc'; Optional = $false } @{ Name = 'WdNisDrv'; Optional = $false } @{ Name = 'WdBoot'; Optional = $false } @{ Name = 'WdFilter'; Optional = $false } ) } function Format-StartToken { param($Value) if ($Value -eq 'NOPROP') { return 'absent' } if ($null -eq $Value -or $Value -eq 'UNREADABLE' -or $Value -eq 'NOKEY') { return 'unreadable' } return [string]$Value } function Get-NoTaskWarning { 'WARNING: no Defender tasks were returned. ' + 'Do not treat tasks as disabled. After the normal boot, ' + 'Check_Defender_Disabled.PS1 must show each task Disabled.' } function Get-DefenderRegistryStart { param([Parameter(Mandatory)][string]$Key) if (-not (Test-Path -LiteralPath $Key)) { return 'NOKEY' } try { $item = Get-ItemProperty -LiteralPath $Key -ErrorAction Stop } catch { return 'UNREADABLE' } if ($item.PSObject.Properties.Name -notcontains 'Start') { return 'NOPROP' } return [int]$item.Start } function Set-DefenderServicesDisabled { param( [string]$ServicesRoot = 'HKLM:\SYSTEM\CurrentControlSet\Services', [scriptblock]$WriteStart ) if ($env:DEFENDER_PROCEDURE_UNDER_TEST -eq '1' -and $ServicesRoot -like 'HKLM:*') { throw 'Refusing to write HKLM during tests.' } $root = $ServicesRoot.TrimEnd('\') $lines = New-Object System.Collections.Generic.List[string] $failed = $false foreach ($entry in @(Get-DefenderDisablePlan)) { $name = [string]$entry.Name $key = "$root\$name" $current = Get-DefenderRegistryStart -Key $key if ($current -eq 'NOKEY') { [void]$lines.Add("missing: $name") if (-not $entry.Optional) { $failed = $true } continue } if ($current -eq 'UNREADABLE') { [void]$lines.Add("${name}: unreadable -> denied") $failed = $true continue } if ($current -eq 4) { [void]$lines.Add("${name}: 4 -> 4") continue } $before = if ($current -eq 'NOPROP') { 'absent' } else { [string]$current } try { if ($null -ne $WriteStart) { & $WriteStart $key 4 } else { Set-ItemProperty -LiteralPath $key -Name Start -Value 4 -Type DWord -ErrorAction Stop } } catch { $still = Get-DefenderRegistryStart -Key $key $shown = Format-StartToken $still [void]$lines.Add("${name}: $before -> denied (still $shown)") $failed = $true continue } $after = Get-DefenderRegistryStart -Key $key if ($after -ne 4) { $shown = Format-StartToken $after [void]$lines.Add("${name}: $before -> $shown") $failed = $true continue } [void]$lines.Add("${name}: $before -> 4") } [pscustomobject]@{ Lines = $lines.ToArray() Failed = [bool]$failed } } function Disable-DefenderScheduledTasks { param( [scriptblock]$GetTasks, [scriptblock]$DisableOne ) if (-not $GetTasks) { if ($env:DEFENDER_PROCEDURE_UNDER_TEST -eq '1') { throw 'Refusing to query scheduled tasks during tests.' } $GetTasks = { Get-ScheduledTask -TaskPath '\Microsoft\Windows\Windows Defender\' -ErrorAction Stop } } if (-not $DisableOne) { $DisableOne = { param($Task) Disable-ScheduledTask -InputObject $Task -ErrorAction Stop } } $lines = New-Object System.Collections.Generic.List[string] $failed = $false $tasks = @() try { $tasks = @(& $GetTasks) } catch { $tasks = @() [void]$lines.Add((Get-NoTaskWarning)) return [pscustomobject]@{ Lines = $lines.ToArray(); Failed = $false; Warning = $true } } $real = @($tasks | Where-Object { $null -ne $_ }) if ($real.Count -eq 0) { [void]$lines.Add((Get-NoTaskWarning)) return [pscustomobject]@{ Lines = $lines.ToArray(); Failed = $false; Warning = $true } } foreach ($task in $real) { $name = [string]$task.TaskName if ([string]::IsNullOrWhiteSpace($name)) { $name = '(unnamed task)' } $updated = $null try { $updated = & $DisableOne $task } catch { [void]$lines.Add("$name = denied") $failed = $true continue } $state = $null if ($null -ne $updated -and $updated.PSObject.Properties.Name -contains 'State') { $state = [string]$updated.State } elseif ($task.PSObject.Properties.Name -contains 'State') { $state = [string]$task.State } if ($state -ne 'Disabled') { $shown = if ([string]::IsNullOrWhiteSpace($state)) { 'unknown' } else { $state } [void]$lines.Add("$name = $shown") $failed = $true continue } [void]$lines.Add("$name = Disabled") } [pscustomobject]@{ Lines = $lines.ToArray() Failed = [bool]$failed Warning = $false } } function Invoke-DefenderOff { param( [string]$ServicesRoot = 'HKLM:\SYSTEM\CurrentControlSet\Services', [scriptblock]$WriteStart, [scriptblock]$GetTasks, [scriptblock]$DisableOne ) $svcParams = @{ ServicesRoot = $ServicesRoot } if ($PSBoundParameters.ContainsKey('WriteStart')) { $svcParams.WriteStart = $WriteStart } $services = Set-DefenderServicesDisabled @svcParams $taskParams = @{} if ($PSBoundParameters.ContainsKey('GetTasks')) { $taskParams.GetTasks = $GetTasks } if ($PSBoundParameters.ContainsKey('DisableOne')) { $taskParams.DisableOne = $DisableOne } $tasks = Disable-DefenderScheduledTasks @taskParams $lines = New-Object System.Collections.Generic.List[string] foreach ($line in @($services.Lines)) { [void]$lines.Add([string]$line) } foreach ($line in @($tasks.Lines)) { [void]$lines.Add([string]$line) } $failed = [bool]$services.Failed -or [bool]$tasks.Failed if ($failed) { [void]$lines.Add('RESULT: FAIL') [void]$lines.Add('Do not continue to the next step while RESULT is FAIL.') $code = 1 } else { [void]$lines.Add('RESULT: PASS') [void]$lines.Add( 'Final gate: after the normal boot, ' + 'Check_Defender_Disabled.PS1 must print RESULT: PASS.') $code = 0 } [pscustomobject]@{ Lines = $lines.ToArray() Passed = (-not $failed) Code = $code } } # Script-scope entry. Dot-sourcing loads the functions and does not change the machine. # DEFENDER_PROCEDURE_UNDER_TEST is set only by the offline test runner. if ($MyInvocation.InvocationName -ne '.') { if ($env:DEFENDER_PROCEDURE_UNDER_TEST -eq '1') { throw 'Refusing to run the live procedure during tests.' } $liveResult = Invoke-DefenderOff foreach ($liveLine in @($liveResult.Lines)) { Write-Output $liveLine } if ($PSCommandPath) { exit $liveResult.Code } }