#Requires -RunAsAdministrator <# .SYNOPSIS Turn Smart App Control back on (enforce or evaluation). .DESCRIPTION Restores SAC by writing VerifiedAndReputablePolicyState. -Mode Enforce -> 1 (default) -Mode Evaluation -> 2 Also writes VerifiedAndReputablePolicyStateMinValueSeen to the same value so Windows treats the choice as explicit. On builds before the March/April 2026 cumulative updates, the Windows Security toggle cannot return from Off. This registry write is the supported testing path; if it does not stick, the only reliable revert is Reset this PC / clean install with optional diagnostic data enabled. Enterprise management can force SAC off again within about 48 hours. Run from an elevated PowerShell window on the normal desktop. Safe Mode is not required. Reboot after the write. .PARAMETER Mode Enforce or Evaluation. Default is Enforce. .EXAMPLE .\Enable-SmartAppControl.ps1 .\Enable-SmartAppControl.ps1 -Mode Evaluation #> [CmdletBinding()] param( [ValidateSet('Enforce', 'Evaluation')] [string]$Mode = 'Enforce' ) $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() } function Get-SacModeValue { param([string]$Mode) if ($Mode -eq 'Evaluation') { return 2 } return 1 } # File run performs the restore. Dot-sourcing only loads the functions. if ($MyInvocation.InvocationName -ne '.') { Assert-Admin $target = Get-SacModeValue -Mode $Mode $policyKey = 'HKLM:\SYSTEM\CurrentControlSet\Control\CI\Policy' $protectedKey = 'HKLM:\SYSTEM\CurrentControlSet\Control\CI\Protected' $stateName = 'VerifiedAndReputablePolicyState' $minSeenName = 'VerifiedAndReputablePolicyStateMinValueSeen' $stamp = Get-Date -Format 'yyyyMMdd-HHmmss' $backupPath = Join-Path $PSScriptRoot "SAC-backup-before-enable-$stamp.json" $backup = [ordered]@{ CapturedAt = (Get-Date).ToString('o') Action = "enable-$Mode" Target = $target Values = [ordered]@{ PolicyState = Get-DwordOrNull -Path $policyKey -Name $stateName MinSeen = Get-DwordOrNull -Path $protectedKey -Name $minSeenName } } $backup | ConvertTo-Json | Set-Content -LiteralPath $backupPath -Encoding UTF8 Write-Host "Backup written: $backupPath" $diag = Get-ItemProperty -LiteralPath 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\DataCollection' -Name AllowTelemetry -ErrorAction SilentlyContinue $userDiag = Get-ItemProperty -LiteralPath 'HKCU:\Software\Microsoft\Windows\CurrentVersion\Privacy' -Name TailoredExperiencesWithDiagnosticDataEnabled -ErrorAction SilentlyContinue $telemetry = if ($null -eq $diag) { '' } else { [string]$diag.AllowTelemetry } $tailored = if ($null -eq $userDiag) { '' } else { [string]$userDiag.TailoredExperiencesWithDiagnosticDataEnabled } Write-Host ("AllowTelemetry = {0}. Optional diagnostic data is 3." -f $telemetry) Write-Host ("TailoredExperiencesWithDiagnosticDataEnabled = {0}" -f $tailored) Write-Host 'Optional diagnostic data must be on or SAC will refuse to stay on.' Write-Host 'Confirm in Settings > Privacy & security > Diagnostics & feedback.' Write-Host ("Setting Smart App Control to {0} ({1})..." -f $Mode, $target) Set-Dword -Path $policyKey -Name $stateName -Value $target $protectedOk = $true try { Set-Dword -Path $protectedKey -Name $minSeenName -Value $target } 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 $target } catch { $protectedOk = $false Write-Warning "Could not write $minSeenName under CI\Protected. Policy value was still set. If the UI stays Off after reboot, use Reset this PC. 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)) } Write-Host '' Write-Host 'Reboot required. After reboot check Windows Security > App & browser control,' Write-Host 'and run: CiTool.exe -lp' if ($Mode -eq 'Enforce') { Write-Host 'Enforce looks like Friendly Name VerifiedAndReputableDesktop, Is Currently Enforced = true.' } else { Write-Host 'Evaluation looks like Friendly Name VerifiedAndReputableDesktopEvaluation, Is Currently Enforced = true.' } Write-Host 'If the value reverts to 0, this install is not eligible. Reset this PC (Keep my files) is the remaining path.' $answer = Read-Host 'Reboot now? [y/N]' if ($answer -match '^(y|yes)$') { Restart-Computer -Force } }