#Requires -RunAsAdministrator <# .SYNOPSIS Force Smart App Control off and keep it off. .DESCRIPTION Sets the SAC policy state to Off (0), records that Off was explicitly chosen, and stops Defender learning mode from rewriting the state. Backs up the current values next to this script before changing anything. Values: VerifiedAndReputablePolicyState 0 = Off 1 = Enforce 2 = Evaluation This is the documented admin/testing mechanism. There is no SAC Group Policy. On enterprise-managed devices Windows may already turn SAC off by itself. A bad partial edit of these keys can leave code integrity blocking almost everything. The backup file is the rollback if this script fails midway. .NOTES Run from an elevated PowerShell window on the normal desktop. Safe Mode is not required and does not help these writes. Reboot is required for the policy to load. CiTool -r is attempted but is not a substitute for a restart. #> Set-ExecutionPolicy -Scope Process Bypass -Force $ErrorActionPreference = 'Stop' function Assert-Admin { $identity = [Security.Principal.WindowsIdentity]::GetCurrent() $principal = New-Object Security.Principal.WindowsPrincipal($identity) if (-not $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) { throw 'Run this script from an elevated PowerShell window.' } } function Get-DwordOrNull { param([string]$Path, [string]$Name) if (-not (Test-Path -LiteralPath $Path)) { return $null } $item = Get-ItemProperty -LiteralPath $Path -Name $Name -ErrorAction SilentlyContinue if ($null -eq $item) { return $null } return [int]$item.$Name } function Set-Dword { param([string]$Path, [string]$Name, [int]$Value) if (-not (Test-Path -LiteralPath $Path)) { New-Item -Path $Path -Force | Out-Null } New-ItemProperty -LiteralPath $Path -Name $Name -PropertyType DWord -Value $Value -Force | Out-Null } function Enable-Privilege { param([string]$Name) $definition = @' using System; using System.Runtime.InteropServices; public class SacPriv { [DllImport("advapi32.dll", SetLastError=true)] public static extern bool OpenProcessToken(IntPtr ProcessHandle, uint DesiredAccess, out IntPtr TokenHandle); [DllImport("advapi32.dll", SetLastError=true, CharSet=CharSet.Unicode)] public static extern bool LookupPrivilegeValue(string lpSystemName, string lpName, out long lpLuid); [DllImport("advapi32.dll", SetLastError=true)] public static extern bool AdjustTokenPrivileges(IntPtr TokenHandle, bool DisableAllPrivileges, ref TOKEN_PRIVILEGES NewState, uint BufferLength, IntPtr PreviousState, IntPtr ReturnLength); [DllImport("kernel32.dll")] public static extern IntPtr GetCurrentProcess(); [DllImport("kernel32.dll", SetLastError=true)] public static extern bool CloseHandle(IntPtr hObject); [StructLayout(LayoutKind.Sequential, Pack=4)] public struct TOKEN_PRIVILEGES { public uint PrivilegeCount; public uint LuidLow; public int LuidHigh; public uint Attributes; } } '@ if (-not ('SacPriv' -as [type])) { Add-Type -TypeDefinition $definition | Out-Null } $token = [IntPtr]::Zero try { $opened = [SacPriv]::OpenProcessToken([SacPriv]::GetCurrentProcess(), 0x28, [ref]$token) if (-not $opened) { throw "OpenProcessToken failed: $([Runtime.InteropServices.Marshal]::GetLastWin32Error())" } $luid = [int64]0 if (-not [SacPriv]::LookupPrivilegeValue($null, $Name, [ref]$luid)) { throw "LookupPrivilegeValue failed for $Name" } $bytes = [BitConverter]::GetBytes($luid) $tp = New-Object SacPriv+TOKEN_PRIVILEGES $tp.PrivilegeCount = 1 $tp.LuidLow = [BitConverter]::ToUInt32($bytes, 0) $tp.LuidHigh = [BitConverter]::ToInt32($bytes, 4) $tp.Attributes = 0x2 $adjusted = [SacPriv]::AdjustTokenPrivileges( $token, $false, [ref]$tp, 0, [IntPtr]::Zero, [IntPtr]::Zero) $err = [Runtime.InteropServices.Marshal]::GetLastWin32Error() if (-not $adjusted -or $err -ne 0) { throw "AdjustTokenPrivileges failed for $Name (Win32 $err)." } } finally { if ($token -ne [IntPtr]::Zero) { [void][SacPriv]::CloseHandle($token) } } } function Set-ProtectedDword { param([string]$SubKey, [string]$Name, [int]$Value) Enable-Privilege -Name 'SeTakeOwnershipPrivilege' Enable-Privilege -Name 'SeRestorePrivilege' Enable-Privilege -Name 'SeBackupPrivilege' $takeRights = [System.Security.AccessControl.RegistryRights]::TakeOwnership -bor ` [System.Security.AccessControl.RegistryRights]::ReadPermissions $base = [Microsoft.Win32.Registry]::LocalMachine.OpenSubKey( $SubKey, [Microsoft.Win32.RegistryKeyPermissionCheck]::ReadWriteSubTree, $takeRights ) if (-not $base) { throw "Could not open $SubKey for TakeOwnership." } $admins = New-Object System.Security.Principal.SecurityIdentifier('S-1-5-32-544') $acl = $base.GetAccessControl() $acl.SetOwner($admins) $base.SetAccessControl($acl) $base.Close() $changeRights = [System.Security.AccessControl.RegistryRights]::ChangePermissions -bor ` [System.Security.AccessControl.RegistryRights]::ReadPermissions $writable = [Microsoft.Win32.Registry]::LocalMachine.OpenSubKey( $SubKey, [Microsoft.Win32.RegistryKeyPermissionCheck]::ReadWriteSubTree, $changeRights ) if (-not $writable) { throw "Could not open $SubKey for ChangePermissions." } $acl = $writable.GetAccessControl() $rule = New-Object System.Security.AccessControl.RegistryAccessRule( $admins, [System.Security.AccessControl.RegistryRights]::FullControl, [System.Security.AccessControl.InheritanceFlags]::None, [System.Security.AccessControl.PropagationFlags]::None, [System.Security.AccessControl.AccessControlType]::Allow ) $acl.AddAccessRule($rule) $writable.SetAccessControl($acl) $writable.Close() $key = [Microsoft.Win32.Registry]::LocalMachine.OpenSubKey($SubKey, $true) if (-not $key) { throw "Could not open $SubKey for write." } $key.SetValue($Name, $Value, [Microsoft.Win32.RegistryValueKind]::DWord) $key.Close() } # File run performs the disable. Dot-sourcing only loads the functions. if ($MyInvocation.InvocationName -ne '.') { Assert-Admin $policyKey = 'HKLM:\SYSTEM\CurrentControlSet\Control\CI\Policy' $protectedKey = 'HKLM:\SYSTEM\CurrentControlSet\Control\CI\Protected' $defenderKey = 'HKLM:\SOFTWARE\Microsoft\Windows Defender' $stateName = 'VerifiedAndReputablePolicyState' $minSeenName = 'VerifiedAndReputablePolicyStateMinValueSeen' $learningName = 'SacLearningModeSwitch' $stamp = Get-Date -Format 'yyyyMMdd-HHmmss' $backupPath = Join-Path $PSScriptRoot "SAC-backup-before-disable-$stamp.json" $backup = [ordered]@{ CapturedAt = (Get-Date).ToString('o') Action = 'disable' Values = [ordered]@{ PolicyState = Get-DwordOrNull -Path $policyKey -Name $stateName MinSeen = Get-DwordOrNull -Path $protectedKey -Name $minSeenName Learning = Get-DwordOrNull -Path $defenderKey -Name $learningName } } $backup | ConvertTo-Json | Set-Content -LiteralPath $backupPath -Encoding UTF8 Write-Host "Backup written: $backupPath" Write-Host 'Setting Smart App Control to Off (0)...' Set-Dword -Path $policyKey -Name $stateName -Value 0 $protectedOk = $true try { Set-Dword -Path $protectedKey -Name $minSeenName -Value 0 } catch { Write-Host 'CI\Protected is owned by TrustedInstaller. Taking ownership and retrying...' try { Set-ProtectedDword -SubKey 'SYSTEM\CurrentControlSet\Control\CI\Protected' -Name $minSeenName -Value 0 } catch { $protectedOk = $false Write-Warning "Could not write $minSeenName under CI\Protected. Policy Off was still set. If SAC comes back after reboot, use the offline WinRE hive edit. Error: $($_.Exception.Message)" } } $learningOk = $true try { Set-Dword -Path $defenderKey -Name $learningName -Value 0 } catch { $learningOk = $false $tamper = $null try { $tamper = (Get-MpComputerStatus -ErrorAction Stop).IsTamperProtected } catch { } if ($tamper) { Write-Warning "Could not write $learningName because Tamper Protection is on. Policy Off was still set. Turn Tamper Protection off in Windows Security > Virus & threat protection > Manage settings, rerun this script, then turn Tamper Protection back on." } else { Write-Warning "Could not write $learningName. Defender learning mode may rewrite SAC state. Error: $($_.Exception.Message)" } } $citool = Join-Path $env:SystemRoot 'System32\CiTool.exe' if (Test-Path -LiteralPath $citool) { Write-Host 'Refreshing code integrity policy (CiTool -r)...' & $citool -r if ($LASTEXITCODE -ne 0) { Write-Warning "CiTool -r exited $LASTEXITCODE. Reboot anyway." } } else { Write-Warning 'CiTool.exe not found. Reboot to load the policy.' } Write-Host '' Write-Host 'Current values:' Write-Host (" {0} = {1}" -f $stateName, (Get-DwordOrNull -Path $policyKey -Name $stateName)) if ($protectedOk) { Write-Host (" {0} = {1}" -f $minSeenName, (Get-DwordOrNull -Path $protectedKey -Name $minSeenName)) } if ($learningOk) { Write-Host (" {0} = {1}" -f $learningName, (Get-DwordOrNull -Path $defenderKey -Name $learningName)) } Write-Host '' Write-Host 'Smart App Control is set to Off. A reboot is required.' Write-Host 'After reboot, confirm with: CiTool.exe -lp' Write-Host 'You do not want VerifiedAndReputableDesktop listed as currently enforced.' $answer = Read-Host 'Reboot now? [y/N]' if ($answer -match '^(y|yes)$') { Restart-Computer -Force } }