← All posts

Hardening PowerShell: The Attacker's Favorite Post-Exploitation Tool Is Your Best Defensive Asset

PowerShell is simultaneously the most abused tool in post-exploitation kits and the most powerful defensive instrumentation on Windows. Here's how to configure it so it works for you, not against you.

Every red team engagement I've been part of in the last five years has used PowerShell. Every single one. It's pre-installed, it's trusted by application control policies, it has full access to .NET and WMI, and on most machines it logs nothing. From an attacker's perspective, it's the perfect living-off-the-land binary.

But here's what most defenders miss: PowerShell is also the only Windows subsystem that offers full keystroke-level logging of attacker behavior — if you turn it on. A properly hardened PowerShell configuration transforms the attacker's favorite tool into a forensic tripwire that records everything they do.

The Five Controls That Matter

PowerShell hardening isn't about restricting functionality (that breaks admin workflows). It's about ensuring visibility and limiting the execution surface. Five specific controls cover 95% of the defensive value:

1. Script Block Logging

This is the single most important PowerShell security control. When enabled, every script block that PowerShell executes gets logged to the Microsoft-Windows-PowerShell/Operational event log — including dynamically generated code, decoded Base64 payloads, and deobfuscated strings.

Why it matters: attackers routinely encode their payloads to evade static detection. A command like powershell -enc SQBFAFgA... looks opaque in process logs. But Script Block Logging records the decoded output — the actual malicious commands — because logging happens after PowerShell's parser processes the input.

# Enable via registry (immediate, no reboot)
Set-ItemProperty -Path "HKLM:\SOFTWARE\Policies\Microsoft\Windows\PowerShell\ScriptBlockLogging" `
  -Name "EnableScriptBlockLogging" -Value 1 -Type DWord -Force

# Verify
Get-ItemProperty "HKLM:\SOFTWARE\Policies\Microsoft\Windows\PowerShell\ScriptBlockLogging"
# EnableScriptBlockLogging: 1

WinSentinel's PowerShell Security module checks this automatically and flags it as Critical if disabled. This is non-negotiable on any machine you care about defending.

2. Transcription Logging

Script Block Logging captures what PowerShell executes. Transcription captures the full interactive session — input, output, timestamps. Think of it as a screen recording of every PowerShell console session on the machine.

# Enable transcription to a protected directory
New-Item -Path "C:\PSTranscripts" -ItemType Directory -Force
icacls "C:\PSTranscripts" /inheritance:r /grant "SYSTEM:(OI)(CI)F" /grant "Administrators:(OI)(CI)F"

Set-ItemProperty -Path "HKLM:\SOFTWARE\Policies\Microsoft\Windows\PowerShell\Transcription" `
  -Name "EnableTranscripting" -Value 1 -Type DWord -Force
Set-ItemProperty -Path "HKLM:\SOFTWARE\Policies\Microsoft\Windows\PowerShell\Transcription" `
  -Name "OutputDirectory" -Value "C:\PSTranscripts" -Type String -Force
Set-ItemProperty -Path "HKLM:\SOFTWARE\Policies\Microsoft\Windows\PowerShell\Transcription" `
  -Name "EnableInvocationHeader" -Value 1 -Type DWord -Force

The invocation header adds timestamps and the user context to each transcript block. Critical for incident timeline reconstruction.

Important: Set ACLs on the transcript directory so standard users can write but not read or delete. Attackers who gain user-level access shouldn't be able to tamper with their own forensic trail.

3. Module Logging

Module logging records which PowerShell modules and cmdlets are invoked, along with their parameters. It's less granular than Script Block Logging but provides structured event data that integrates well with SIEM correlation rules.

# Enable for all modules
Set-ItemProperty -Path "HKLM:\SOFTWARE\Policies\Microsoft\Windows\PowerShell\ModuleLogging" `
  -Name "EnableModuleLogging" -Value 1 -Type DWord -Force

# Log all modules (wildcard)
New-Item -Path "HKLM:\SOFTWARE\Policies\Microsoft\Windows\PowerShell\ModuleLogging\ModuleNames" -Force
Set-ItemProperty -Path "HKLM:\SOFTWARE\Policies\Microsoft\Windows\PowerShell\ModuleLogging\ModuleNames" `
  -Name "*" -Value "*" -Type String -Force

The wildcard means every module gets logged. On busy admin machines this generates volume, but the forensic value is immense. Target specific modules (like Microsoft.PowerShell.Management, NetTCPIP) if log volume is a concern.

4. Constrained Language Mode

This is the most aggressive control and the one most likely to break legitimate workflows. Constrained Language Mode (CLM) restricts PowerShell to a subset of the language — no direct .NET method calls, no COM objects, no Add-Type compilations. This cripples 90% of offensive PowerShell tooling (Mimikatz loaders, AMSI bypasses, reflective injection) while leaving basic administrative cmdlets functional.

# Check current language mode
$ExecutionContext.SessionState.LanguageMode
# Should return: ConstrainedLanguage (on locked-down machines)
# Typically returns: FullLanguage (on most machines)

Enforcing CLM properly requires Windows Defender Application Control (WDAC) or AppLocker in Allow mode. Without an application control policy backing it, CLM can be trivially bypassed by spawning a new powershell.exe process. WinSentinel checks whether CLM is enforced and whether the enforcement mechanism is sound.

Recommendation: Enable CLM on endpoint machines (standard users). Leave FullLanguage on admin jump boxes where you need the flexibility. WinSentinel flags FullLanguage as a Warning (not Critical) because the right answer depends on the machine's role.

5. AMSI Integrity

The Antimalware Scan Interface (AMSI) is Windows' hook that lets Defender (or any AV) inspect PowerShell commands before execution. Attackers bypass AMSI as their first post-exploitation step — usually with a one-liner that patches the amsi.dll scan function in memory.

You can't prevent AMSI bypasses purely through configuration (they exploit in-memory patching). But you can ensure AMSI is at least present and functional at session start:

# Verify AMSI is loaded and functional
[Ref].Assembly.GetType('System.Management.Automation.AmsiUtils') # Should exist
# WinSentinel checks for known AMSI bypass artifacts in PowerShell profiles

WinSentinel also inspects all PowerShell profile files ($PROFILE, AllUsersAllHosts, etc.) for known AMSI bypass patterns. Attackers frequently drop persistence in profile scripts that disable AMSI on every new session.

What About Execution Policy?

Execution policy is not a security boundary. Microsoft says so explicitly in their documentation. It's a safety net to prevent users from accidentally running scripts, not a defense against attackers. Any attacker can bypass it with powershell -ExecutionPolicy Bypass or by piping content directly.

That said, Unrestricted is still worse than RemoteSigned because it removes even the accidental-execution protection. WinSentinel flags Unrestricted as a Warning and recommends RemoteSigned as the sane default for admin machines.

# Set to RemoteSigned (allows local scripts, requires signatures on downloaded scripts)
Set-ExecutionPolicy RemoteSigned -Scope LocalMachine -Force

The WinSentinel PowerShell Module Check

Running winsentinel --audit --modules powershell checks all five controls in under 2 seconds:

$ winsentinel --audit --modules powershell

 PowerShell Security
 ─────────────────────────────────────────────────────
 [CRITICAL] Script Block Logging is disabled
   → Attacker commands will not be recorded
   → Fix: Enable via registry or GPO

 [CRITICAL] Transcription logging is disabled
   → No session recording for forensics
   → Fix: Set OutputDirectory and enable

 [WARNING] Module Logging is disabled
   → Cmdlet invocations not captured
   → Fix: Enable with wildcard module list

 [WARNING] Execution Policy set to Unrestricted
   → No protection against accidental script execution
   → Fix: Set-ExecutionPolicy RemoteSigned

 [INFO]    Language Mode: FullLanguage
   → .NET and COM access available to all scripts
   → Consider CLM with WDAC on endpoint machines

Auto-fix with winsentinel --fix --modules powershell enables Script Block Logging, Transcription, and Module Logging, and sets execution policy to RemoteSigned. It won't enable CLM automatically because that requires application control policy validation first.

Why This Matters More Than You Think

According to the 2025 Mandiant M-Trends report, PowerShell was used in 68% of intrusions involving Windows endpoints. In 74% of those cases, no PowerShell logging was configured — meaning defenders had zero forensic evidence of what the attacker executed.

The irony is brutal: the tool that gives attackers the most power is also the tool that can give defenders the most visibility. The difference is three registry keys. Three keys that take 30 seconds to set and make the difference between "we have no idea what the attacker did" and "here's a full transcript of every command they ran."

Run winsentinel --audit --modules powershell right now. If Script Block Logging isn't enabled, you're flying blind to the most common post-exploitation technique on Windows. Fix it today.