HOW-TO GUIDE | ENDPOINT SECURITY
Updated 9 min read

How to Harden PowerShell: Constrained Language Mode, Script Block Logging, and AMSI

Event 4104
PowerShell Script Block Logging event: logs every script block executed to Microsoft-Windows-PowerShell/Operational; captures de-obfuscated code even when the input was base64-encoded
CLM
Constrained Language Mode: restricts PowerShell to a safe subset of the language, blocking Add-Type (C# compilation), COM objects, .NET type access, and unsafe reflection
AMSI
Antimalware Scan Interface: a Windows API that routes PowerShell script content through the registered AV engine before execution; attackers actively try to bypass it
4104
Event ID for PowerShell Script Block Logging: logged to Microsoft-Windows-PowerShell/Operational; captures the complete script content executed, enabling detection of fileless malware

SponsoredRetool

Retool's new app builder is where AI-generated code ships safely

Building apps with AI is easy. Getting them to production safely is another story.

Start building for free today

PowerShell is the most abused legitimate tool in Windows enterprise environments. It runs with full access to the Windows API, is trusted by most application control solutions, and can download and execute code entirely in memory: making it ideal for fileless malware, C2 communication, lateral movement, and credential dumping. Most attacker PowerShell techniques rely on capabilities that Constrained Language Mode blocks.

The four-control hardening approach is defense in depth: CLM prevents many attack techniques outright, Script Block Logging captures what CLM does not block, Module Logging provides operational visibility, and AMSI routes everything through the installed AV engine for signature-based detection.

Control 1: Script Block Logging (Enable First)

Script Block Logging is the highest-priority PowerShell security control: it captures the complete de-obfuscated content of every script block executed, even when the attacker used base64 encoding or other obfuscation.

GPO path:
Computer Configuration > Administrative Templates >
Windows Components > Windows PowerShell >
"Turn on PowerShell Script Block Logging"
Setting: Enabled
"Log script block invocation start / stop events": Enabled (optional: adds Event 4105/4106)

Results in Event ID 4104 logged to:
Microsoft-Windows-PowerShell/Operational
# Verify Script Block Logging is enabled on a machine:
Get-ItemProperty -Path 'HKLM:\SOFTWARE\Policies\Microsoft\Windows\PowerShell\ScriptBlockLogging' \
  -Name EnableScriptBlockLogging
# Value 1 = enabled

# Test: run an obfuscated command and verify it's captured de-obfuscated:
powershell.exe -enc JABhACAAPQAgACIAdABlAHMAdAAiADsAIABXAHIAaQB0AGUALQBIAGUAYwB0ACAkAGE
# Check the event log:
Get-WinEvent -LogName 'Microsoft-Windows-PowerShell/Operational' |
  Where-Object { $_.Id -eq 4104 } | 
  Select-Object -First 1 | Format-List
# The logged content should show the DECODED script, not the base64

Query Script Block Logging in Sentinel:

// Detect suspicious PowerShell in script block logs
Event
| where Source == "Microsoft-Windows-PowerShell"
| where EventID == 4104
| where TimeGenerated > ago(24h)
// Look for attacker-common strings in the decoded script:
| where RenderedDescription has_any (
    "Invoke-Mimikatz", "Invoke-ReflectivePEInjection",
    "IEX", "Invoke-Expression",
    "DownloadString", "WebClient",
    "AmsiUtils", "amsiInitFailed",  // AMSI bypass attempts
    "System.Reflection.Assembly::Load"
)
| project TimeGenerated, Computer,
    ScriptContent = RenderedDescription

Control 2: Module Logging

Module logging records every PowerShell module and function called, providing a trace of what the script did even when script block content is not available.

GPO path:
Computer Configuration > Administrative Templates >
Windows Components > Windows PowerShell >
"Turn on Module Logging"
Setting: Enabled
Module Names: * (wildcard: log all modules)

Results in Event ID 4103 logged to:
Microsoft-Windows-PowerShell/Operational
# Verify Module Logging is enabled:
Get-ItemProperty -Path 'HKLM:\SOFTWARE\Policies\Microsoft\Windows\PowerShell\ModuleLogging' \
  -Name EnableModuleLogging
# Value 1 = enabled

# Test: execute a module function and verify it appears in Event 4103:
Get-ChildItem -Path C:\
Get-WinEvent -LogName 'Microsoft-Windows-PowerShell/Operational' |
  Where-Object { $_.Id -eq 4103 } | Select-Object -First 3

Increase PowerShell Event Log Size (critical for IR retention):

GPO path:
Computer Configuration > Administrative Templates >
Windows Components > Event Log Service >
Microsoft-Windows-PowerShell/Operational
"Maximum Log Size (KB)": 102400  (= 100 MB)
"Retention Method": Overwrite events as needed

# Default is 15MB: too small to retain 24 hours of PowerShell activity
# 100MB provides ~7 days retention for most environments
Free daily briefing

Briefings like this, every morning before 9am.

Threat intel, active CVEs, and campaign alerts, distilled for practitioners. 50,000+ subscribers. No noise.

Control 3: Constrained Language Mode (CLM)

CLM restricts PowerShell to a safe subset of the language: blocking the capabilities that most attack tooling requires. It is enforced automatically when Windows Defender Application Control (WDAC) is active with script enforcement, or can be set via a registry key (less secure: attackers can unset it).

CLM capabilities blocked:

  • Add-Type: C# compilation (used by PowerSploit, Empire, many C2 agents)
  • .NET type access: [System.Reflection.Assembly]::Load, etc.
  • COM object creation: New-Object -ComObject
  • Unsafe reflection
  • WMI cmdlets from WBEM namespace

Enable CLM via WDAC (strongly recommended over registry):

# CLM is automatically enforced when WDAC is active in Enforce mode
# See: how-to-build-wdac-policy-windows-defender-application-control
# When WDAC script enforcement is enabled, PowerShell automatically enters CLM

# Verify CLM is active:
[System.Management.Automation.LanguageMode]::FullLanguage  # Expected in CLM: error
$ExecutionContext.SessionState.LanguageMode
# Returns: ConstrainedLanguage (if CLM active) or FullLanguage (if not)

Test what CLM blocks (verify your enforcement):

# These should FAIL in CLM:
Add-Type -TypeDefinition 'public class Test {}'
# Error: Operation is not allowed in ConstrainedLanguage mode.

[System.Net.WebClient]::new().DownloadString('http://test.com')
# Error: Cannot create type.

$wsh = New-Object -ComObject WScript.Shell
# Error: COM object creation is not allowed in this language mode.

# These should SUCCEED in CLM (safe PowerShell operations):
Get-ChildItem
Get-Process
Where-Object, Select-Object, ForEach-Object, etc.

Detect CLM bypass attempts:

// Script block logs showing CLM bypass techniques:
Event
| where Source == "Microsoft-Windows-PowerShell" and EventID == 4104
| where RenderedDescription has_any (
    "FullLanguage",
    "__PSLockDownPolicy",
    "REGDB_E",
    "bypass",
    "SetLanguageMode"
)
| project TimeGenerated, Computer, RenderedDescription

Control 4: AMSI and Additional Hardening

AMSI routes PowerShell script content through the registered AV engine before execution. Modern EDR solutions (Defender, CrowdStrike, SentinelOne) hook AMSI to inspect scripts.

Verify AMSI is active:

# Test AMSI is working (uses EICAR-equivalent test string):
# This WILL be detected by AV if AMSI is working:
'Invoke-Mimikatz'  # May trigger AMSI alert
# Or use the official AMSI test:
[Ref].Assembly.GetType('System.Management.Automation.AmsiUtils').GetField('amsiInitFailed','NonPublic,Static').SetValue($null,$true)
# If AMSI is working: this generates Event ID 4104 AND an AV alert
# If AMSI is bypassed: this succeeds silently

# Detect AMSI bypass attempts in script block logs:
Get-WinEvent -LogName 'Microsoft-Windows-PowerShell/Operational' |
  Where-Object { $_.Id -eq 4104 -and $_.Message -match 'amsiInitFailed|amsiContext|AmsiUtils' } |
  Select-Object TimeCreated, Message

Force PowerShell 5.x (block downgrade to v2):

PowerShell 2.0 does not support AMSI or Script Block Logging: attackers downgrade to it to bypass these controls.

# Verify PowerShell 2.0 is disabled:
Get-WindowsOptionalFeature -Online -FeatureName 'MicrosoftWindowsPowerShellV2'
# State: Disabled = protected

# Disable PowerShell 2.0 if enabled:
Disable-WindowsOptionalFeature -Online -FeatureName 'MicrosoftWindowsPowerShellV2'
Disable-WindowsOptionalFeature -Online -FeatureName 'MicrosoftWindowsPowerShellV2Root'

# Via GPO: use Software Restriction Policies or WDAC to block powershell.exe with
# -Version 2 argument

Configure PowerShell execution policy (defense in depth: not a security boundary):

GPO path:
Computer Configuration > Administrative Templates >
Windows Components > Windows PowerShell >
"Turn on Script Execution"
Setting: Enabled
Execution Policy: AllSigned

# AllSigned: all scripts must be digitally signed: blocks unsigned attacker scripts
# Note: execution policy is NOT a security boundary: easily bypassed:
# powershell.exe -ExecutionPolicy Bypass -File script.ps1
# It is a speedbump, not a wall. CLM + AMSI + Script Block Logging are the real controls.

The bottom line

PowerShell security hardening requires four controls deployed via GPO: Script Block Logging (Event 4104, captures de-obfuscated code), Module Logging (Event 4103, records all cmdlets called), Constrained Language Mode (enforced via WDAC, blocks Add-Type and .NET type access used by attack tools), and AMSI integration (routes scripts through AV engine). Disable PowerShell 2.0 (Disable-WindowsOptionalFeature -FeatureName MicrosoftWindowsPowerShellV2) to prevent downgrade attacks that bypass logging and AMSI. Increase the PowerShell/Operational event log size to 100MB via GPO to retain sufficient history for IR.

Frequently asked questions

What is PowerShell Constrained Language Mode and how does it help?

Constrained Language Mode (CLM) restricts PowerShell to a safe subset of the language: blocking Add-Type (C# compilation used by most post-exploitation tools), COM object creation, and .NET type access. Most PowerShell-based attacker tools (Invoke-Mimikatz, PowerSploit, Empire agents) fail immediately under CLM. It is enforced automatically when Windows Defender Application Control (WDAC) is active in script enforcement mode: the registry-based enforcement method is bypassed by attackers who unset the registry key.

What is Script Block Logging and why does it defeat PowerShell obfuscation?

PowerShell Script Block Logging (Event ID 4104) captures the content of every script block executed by the PowerShell engine after de-obfuscation: meaning that even if an attacker uses base64 encoding, string reversal, or character substitution, the logged content shows the decoded, human-readable script. Enable via GPO: Computer Configuration > Administrative Templates > Windows Components > Windows PowerShell > Turn on PowerShell Script Block Logging = Enabled.

How do I enable PowerShell Constrained Language Mode for all users?

PowerShell Constrained Language Mode (CLM) restricts PowerShell to a safe subset of language features: it blocks Add-Type (prevents loading arbitrary .NET assemblies), P/Invoke (prevents direct Win32 API calls), and complex type operations used in many PowerShell-based attack tools. Enforce CLM via: Windows Defender Application Control (WDAC) policy — when WDAC enforces a policy, PowerShell automatically enters CLM for non-signed scripts; alternatively set the PSMODULEPATH environment variable via Group Policy or configure __PSLockdownPolicy registry key. CLM via WDAC is the recommended approach: it is enforced at the kernel level and cannot be bypassed by calling PowerShell.exe directly.

What is PowerShell over-the-shoulder logging (Transcription) and when should I enable it?

PowerShell Transcription (Event ID 4104's companion, controlled separately) records all input and output of PowerShell sessions to text files: it captures what was typed AND the result, which Script Block Logging does not. Enable via GPO: Computer Configuration > Admin Templates > Windows Components > Windows PowerShell > Turn on PowerShell Transcription = Enabled. Specify an output directory (a centralized log share works well). Transcription is especially valuable for RDP or interactive PowerShell sessions where an analyst or attacker is typing commands interactively. Note: transcription logs can be large — enable only on high-value targets (servers, admin workstations) to manage storage.

What is the AMSI bypass and how can defenders detect it?

AMSI bypass involves an attacker patching the AmsiScanBuffer function in amsi.dll to return 'scan clean' for all content, preventing Windows Defender and other AMSI-integrated security tools from seeing malicious script content. Common bypass techniques: using [Ref].Assembly.GetType method to locate and patch AMSI in memory; using P/Invoke to call VirtualProtect on the AMSI function and overwrite it with a return instruction. Detection: Sysmon Event 10 (process access) where PowerShell.exe is the source and target, with access mask 0x001F0FFF (full access); EDR memory integrity scanning that detects modifications to amsi.dll function code; and alerting on use of documented AMSI bypass strings (many are flagged by Windows Defender itself). Update EDR signatures regularly: AMSI bypass detection is an active cat-and-mouse game.

Does PowerShell 7 (PowerShell Core) support Script Block Logging and Constrained Language Mode like Windows PowerShell 5.1?

PowerShell 7 (cross-platform, based on .NET 6+) supports Script Block Logging via the same Event ID 4104 mechanism and logs to the Microsoft-Windows-PowerShell/Operational event log on Windows. AMSI integration is supported in PowerShell 7 on Windows. However, Constrained Language Mode enforcement has an important gap: WDAC script enforcement applies to PowerShell 7 only if pwsh.exe is explicitly included in the WDAC policy alongside powershell.exe. Environments that deploy WDAC to enforce CLM for Windows PowerShell 5.1 but do not include pwsh.exe in the policy allow attackers to switch to PowerShell 7 to run code that CLM would block in 5.1. Audit which PowerShell versions exist in your environment: 'Get-Command pwsh -ErrorAction SilentlyContinue' and verify your WDAC policy's script enforcement covers both executables. PowerShell 2.0 downgrade is a separate concern and should be disabled regardless of PowerShell 7 deployment.

Sources & references

  1. Microsoft: PowerShell Security Features
  2. Microsoft: Script Block Logging GPO
  3. SANS: PowerShell Security Best Practices

Free resources

25
Free download

Critical CVE Reference Card 2025–2026

25 actively exploited vulnerabilities with CVSS scores, exploit status, and patch availability. Print it, pin it, share it with your SOC team.

No spam. Unsubscribe anytime.

Free download

Ransomware Incident Response Playbook

Step-by-step 24-hour IR checklist covering detection, containment, eradication, and recovery. Built for SOC teams, IR leads, and CISOs.

No spam. Unsubscribe anytime.

Free newsletter

Get threat intel before your inbox does.

50,000+ security professionals read Decryption Digest for early warnings on zero-days, ransomware, and nation-state campaigns. Free, daily, no spam.

Unsubscribe anytime. We never sell your data.

Eric Bang
Author

Founder & Cybersecurity Evangelist, Decryption Digest

Cybersecurity professional with expertise in threat intelligence, vulnerability research, and enterprise security. Covers zero-days, ransomware, and nation-state operations for 50,000+ security professionals every morning.

Black Hat Giveaway

Win a $2,495 Black Hat pass.

Full-access to Black Hat USA 2026 in Las Vegas. Subscribe free to enter.

Joins Decryption Digest daily briefing. Unsubscribe anytime.

Giveaway: Black Hat USA 2026 Full-Access Pass ($2,495 value)

Details →
Daily Briefing

Subscribe to enter the giveaway

Every subscriber is automatically entered. You also get daily threat intel every morning: zero-days, ransomware, and nation-state campaigns. Free. No spam.

Already subscribed? You're already entered.

Giveaway

Win a $2,495 Black Hat USA 2026 pass.