Skip to content
09 · PowerShell SecurityPart 3 of 4Intermediate

Detecting malicious PowerShell with script block logging

Turn PowerShell logs into detections: rebuild 4104 script blocks, use AMSI and engine events, spot obfuscation and unmanaged PowerShell, tune out admin noise.

Florian Amette8 min read

The PowerShell security pillar turns on the logging. This guide is about what to do with it: where the evidence lands, how to rebuild long scripts from fragments, which patterns separate an attacker from an administrator, and how to tune detections so your analysts still read them next month.

The telemetry you have to work with

SourceLogEventWhat it gives you
Script block loggingMicrosoft-Windows-PowerShell/Operational4104De-obfuscated script text, script block ID, path
Module loggingMicrosoft-Windows-PowerShell/Operational4103Pipeline execution with parameter values
Engine lifecycleWindows PowerShell (classic log)400, 403, 600Engine start/stop, HostApplication, EngineVersion
Process creationSecurity4688Command line and parent (with command-line auditing on)
SysmonMicrosoft-Windows-Sysmon/Operational1, 7Process tree with hashes; DLL loads
AMSI via DefenderMicrosoft-Windows-Windows Defender/Operational1116, 1117Malicious script content detected and acted on

PowerShell 7 writes to its own PowerShellCore/Operational log once its event manifest is registered, and uses its own policy settings. If PowerShell 7 is installed, collect both logs or you have a blind spot with a different executable name (pwsh.exe).

All of these are only useful off the host. Forward them with Windows Event Forwarding or your agent, and raise the Operational log size so a busy server doesn't roll over before collection.

Step 1: reassemble split script blocks

Large scripts are split across several 4104 events. Each fragment carries the same ScriptBlock ID and a "part n of m" counter. Analysts reading single events see broken code, and detection rules miss patterns that straddle a boundary. Rebuild them first:

PowerShell
$events = Get-WinEvent -FilterHashtable @{
  LogName = 'Microsoft-Windows-PowerShell/Operational'; Id = 4104
} -MaxEvents 5000

$events | Group-Object { $_.Properties[3].Value } | ForEach-Object {
  $parts = $_.Group | Sort-Object { [int]$_.Properties[0].Value }
  [pscustomobject]@{
    ScriptBlockId = $_.Name
    Time          = ($parts | Select-Object -First 1).TimeCreated
    Path          = $parts[0].Properties[4].Value
    Text          = ($parts | ForEach-Object { $_.Properties[2].Value }) -join ''
  }
}

In a SIEM, do the same with a group-by on the script block ID before matching. An empty Path means the code came from an interactive prompt, -Command, -EncodedCommand or memory rather than a file on disk. That alone is a useful pivot.

Step 2: start with what PowerShell already flags

Even without a policy, PowerShell 5.1 logs script blocks containing known-suspicious terms as warning-level 4104 events. With script block logging enabled, those are still marked as warnings among the verbose ones. They make a low-noise first alert:

PowerShell
Get-WinEvent -FilterHashtable @{
  LogName = 'Microsoft-Windows-PowerShell/Operational'; Id = 4104; Level = 3
} -MaxEvents 200 | Select-Object TimeCreated, MachineName, @{n='Snippet';e={ $_.Properties[2].Value.Substring(0, [Math]::Min(200, $_.Properties[2].Value.Length)) }}

Review every warning-level event for a few weeks. You'll learn which admin scripts trip them and can exclude those by path.

Step 3: hunt for the patterns attackers need

Attack tooling has to do a small number of things, and each leaves words in the script text. Group indicators by what the attacker is trying to achieve:

GoalIndicators in 4104 / command lines
Download and runDownloadString, DownloadData, Net.WebClient, Invoke-WebRequest / iwr piped to Invoke-Expression / iex, Start-BitsTransfer to temp paths
Hide the payloadFromBase64String, -EncodedCommand and its prefixes (-e, -ec, -enc), IO.Compression.DeflateStream, GzipStream, -bxor
Obfuscate stringsHeavy backtick use (`i`e`x), [char] arrays joined with -join, string reversal, format-operator reordering ('{1}{0}' -f), random casing
Run in memory[Reflection.Assembly]::Load, Add-Type with DllImport, VirtualAlloc, GetProcAddress, GetDelegateForFunctionPointer
Blind defencesAmsiUtils, amsiInitFailed, cachedGroupPolicySettings, Set-MpPreference with -Disable* switches, Add-MpPreference -ExclusionPath
Stay hidden-WindowStyle Hidden / -w hidden, -NoProfile, -NonInteractive, -ExecutionPolicy Bypass combined with the above
Harvest credentialsReferences to lsass, MiniDumpWriteDump, Get-Credential loops, SecureString conversions from files

Two things make these rules workable:

  1. Combine indicators. -NoProfile alone is everywhere. -NoProfile plus a hidden window plus a download cradle is not.
  2. Normalise first. Lowercase the text and strip backticks before matching, and match command-line flags by prefix. powershell.exe accepts / as well as -, and partial parameter names.

The open Sigma rule repository has well-maintained PowerShell rules you can translate to your SIEM rather than writing each from scratch.

Step 4: watch the engine, not just the scripts

Some of the best signals are in the classic Windows PowerShell log and in process telemetry rather than in script text:

  • Downgrade attempts. Event 400 with EngineVersion=2.0 means someone started the PowerShell 2.0 engine, which has no script block logging and no AMSI. Alert on every occurrence, and remove the engine as described in the pillar.
  • Unusual hosts. Event 400 and 600 include HostApplication. PowerShell running inside something other than powershell.exe, pwsh.exe or the ISE is suspicious.
  • Unmanaged PowerShell. Tools can load the PowerShell engine into any .NET process. Sysmon event 7 for System.Management.Automation.dll (or its native image) loaded by an unexpected process catches this. Use your Sysmon configuration to include that one DLL rather than logging every image load.
  • Suspicious parents. 4688 or Sysmon 1 where powershell.exe is the child of winword.exe, excel.exe, outlook.exe, mshta.exe, wmiprvse.exe, w3wp.exe or sqlservr.exe. Office-spawned PowerShell is also blocked outright by attack surface reduction rules.

Step 5: use AMSI detections as high-fidelity alerts

The Antimalware Scan Interface hands script content to Defender at run time, after de-obfuscation. When Defender blocks a script, the user sees "This script contains malicious content and has been blocked by your antivirus software", and Defender logs 1116 (detected) and 1117 (action taken). The detection's resource refers to AMSI and the host process rather than a file on disk.

Treat any AMSI detection on a server as an incident, not an antivirus nuisance. Enable the ASR rule Block execution of potentially obfuscated scripts in audit mode first to see what it would catch.

Step 6: detect attempts to switch off the lights

Attackers who know about logging try to disable it:

  • Registry changes under HKLM\SOFTWARE\Policies\Microsoft\Windows\PowerShell (Sysmon 13, or a SACL with 4657).
  • Log clearing: 1102 (Security) and 104 (System, for any other log). See the security event ID reference for the rest of the tampering signals.
  • A host whose 4104 volume drops to zero while it still reports other events. Build a "silent host" check per log, not just per machine.
  • Defender changes: 5001 (real-time protection disabled) and 5007 (configuration changed, including new exclusions).

Step 7: triage a hit

When a rule fires, answer four questions before deciding it's benign:

  1. Who and where? Account, host and logon session. Pivot on the session to find the 4624 event: an interactive admin at a console is a different story from a network logon arriving from a workstation.
  2. What launched it? Walk the parent chain in 4688 or Sysmon 1. A scheduled task, a service, an Office document and a remote WinRM session (wsmprovhost.exe) each point to different entry points.
  3. What did it do next? Look at the reassembled script, then at network connections (Sysmon 3 and 22), file writes (Sysmon 11) and child processes in the minutes that follow.
  4. Is it known? Search the script block hash or distinctive strings across the fleet. The same script on hundreds of machines is usually an agent; the same script on three servers overnight is usually an intrusion.

Record the answer in the rule's tuning notes, so the next analyst doesn't start from scratch.

What breaks

Detection engineering for PowerShell mostly breaks your analysts' patience:

  • Management agents look like malware. Configuration Manager, the Intune Management Extension, monitoring agents and EDR response tooling routinely run PowerShell with -ExecutionPolicy Bypass, -NoProfile and encoded commands. Exclude them by full parent path and signer, never by process name alone.
  • Volume. Full script block logging on a busy management server produces a lot of 4104 events, and so does every DSC or monitoring run. Size the log accordingly and filter at the SIEM, not by turning logging off.
  • Secrets in logs. Script text includes whatever admins type, including passwords. Restrict read access to the PowerShell logs in your SIEM, or use Protected Event Logging.

Verify it

Test each layer on a lab machine before trusting it in production:

PowerShell
# 1. Script block logging: a harmless, easy-to-find marker
powershell.exe -NoProfile -Command "Write-Output 'sbl-canary-2026'"

# 2. AMSI: Microsoft's documented test string, detected but harmless
'AMSI Test Sample: 7e72c3ce-861b-4339-8740-0ac1484c1386'

# 3. Engine downgrade detection, where the v2 engine still exists in the lab
powershell.exe -Version 2 -Command "exit"

Then confirm each event reached the SIEM and fired the expected rule: 4104 containing the canary string, 1116 from Defender, 400 with EngineVersion=2.0. If one of them is missing, fix the pipeline before writing more rules. Constrained Language Mode reduces what attackers can do; detection tells you they tried. More on both on the PowerShell security topic page.

Checklist

  • 4103, 4104, 400/403/600 and PowerShellCore/Operational forwarded in near real time
  • Script blocks reassembled by ScriptBlock ID before rule matching
  • Alert on warning-level 4104 events, tuned by script path
  • Combined-indicator rules for download, obfuscation, in-memory execution and defence evasion
  • Event 400 with EngineVersion 2.0 alerted
  • Sysmon rule for System.Management.Automation.dll loaded by unexpected processes
  • Office, web server and WMI parents of PowerShell alerted
  • AMSI detections (1116/1117) on servers treated as incidents
  • Silent-host and log-tampering detections in place
  • Management agents excluded by full path and signer only

Frequently asked questions

Does script block logging defeat obfuscation?

Largely. PowerShell logs each script block as it is compiled, so layers of encoding, compression and string tricks are logged again once they are unwrapped into new script blocks. The outer, obfuscated layer is still logged too, which is itself a strong signal. Attackers who avoid PowerShell's engine entirely, or disable logging first, are the gap you cover with other telemetry.

Should I alert on every -EncodedCommand?

Not blindly. Management agents, installers and monitoring tools use encoded commands legitimately, so a raw alert floods the queue. Alert on encoded commands whose parent process is unusual (Office, browsers, web servers, WMI), or combine the encoding with other indicators such as downloads, hidden windows or reflection. Baseline known agents by full path and exclude them precisely.

What if an attacker disables script block logging?

They need administrative rights and leave traces: registry changes under the PowerShell policy key, a sudden drop in 4104 volume from a host, cleared logs (1102 or 104), or in-memory tampering patterns that are themselves logged before they take effect. Forward events centrally in near real time so the evidence leaves the host before it can be removed.

Detecting malicious PowerShell with script block logging

Related guides

Audit & Logging

Deploying and tuning Sysmon at scale

Choose a Sysmon configuration, cut noise without creating blind spots, roll out updates safely across thousands of hosts and monitor Sysmon itself.

Intermediate
Audit & Logging

Windows security event IDs that matter

A practical reference to the Windows event IDs worth collecting and alerting on: logons, failure codes, Kerberos, account changes, persistence and tampering.

Foundation