threat hunting

Hunting for Anomalous PowerShell Execution

Hunt for malicious PowerShell activity by analyzing Script Block Logging (Event 4104), Module Logging (Event 4103), and process creation events. The analyst parses Windows Event Log EVTX files to detect obfuscated commands, AMSI bypass attempts, encoded payloads, credential dumping keywords, and suspicious download cradles. Activates for requests involving PowerShell threat hunting, script block analysis, encoded command detection, or AMSI bypass identification.

amsievent-4104evtxobfuscationpowershellscript-block-loggingthreat-hunting
Install this skill
npx skills add mukul975/Anthropic-Cybersecurity-Skills
Framework mappings

Overview

PowerShell Script Block Logging (Event ID 4104) records the full deobfuscated script text executed on a Windows endpoint, making it the primary data source for hunting malicious PowerShell. Combined with Module Logging (4103) and process creation events, analysts can detect encoded commands, AMSI bypass patterns, download cradles, credential theft tools, and fileless attack techniques even when the attacker uses obfuscation layers.

When to Use

  • When investigating security incidents that require hunting for anomalous powershell execution
  • When building detection rules or threat hunting queries for this domain
  • When SOC analysts need structured procedures for this analysis type
  • When validating security monitoring coverage for related attack techniques

Prerequisites

  • Windows Event Log exports (.evtx) from Microsoft-Windows-PowerShell/Operational
  • Python 3.8+ with python-evtx and lxml libraries
  • Script Block Logging enabled via Group Policy
  • Understanding of common PowerShell attack techniques

Steps

  1. Parse EVTX files extracting Event 4104 script block text and metadata
  2. Reassemble multi-part script blocks using ScriptBlock ID correlation
  3. Scan script text for AMSI bypass indicators and obfuscation patterns
  4. Detect encoded command execution and base64 payloads
  5. Identify download cradles, credential dumping, and lateral movement commands
  6. Score and prioritize findings by threat severity

Expected Output

{
  "total_events": 1247,
  "suspicious_events": 23,
  "amsi_bypass_attempts": 2,
  "encoded_commands": 8,
  "download_cradles": 5,
  "credential_access": 3
}
Source materials

References and resources

Everything below is rendered for inspection. Script files are read-only and never run.

References 1

api-reference.md4.1 KB

Hunting for Anomalous PowerShell Execution — API Reference

Windows Event Log IDs

Event ID Log Source Description
4104 Microsoft-Windows-PowerShell/Operational Script Block Logging — full deobfuscated script text
4103 Microsoft-Windows-PowerShell/Operational Module Logging — pipeline execution details
4688 Security Process Creation with command line auditing
800 Windows PowerShell Pipeline execution (classic log)

Event 4104 XML Fields

Field Path Description
ScriptBlockText EventData/Data[@Name='ScriptBlockText'] Full script block content
ScriptBlockId EventData/Data[@Name='ScriptBlockId'] GUID linking multi-part blocks
MessageNumber EventData/Data[@Name='MessageNumber'] Part number for split blocks
MessageTotal EventData/Data[@Name='MessageTotal'] Total parts in split block
Path EventData/Data[@Name='Path'] Script file path (if applicable)

AMSI Bypass Indicators

Indicator Context
System.Management.Automation.AmsiUtils Reflection access to AMSI internals
amsiInitFailed Setting AMSI init flag to bypass scanning
AmsiScanBuffer Patching the scan buffer function
amsi.dll Direct DLL manipulation
VirtualProtect Memory protection change for AMSI patching
Marshal::Copy Overwriting AMSI function bytes in memory

Suspicious PowerShell Keywords

Keyword Category
Invoke-Mimikatz Credential Dumping
Invoke-Kerberoast Credential Access
Invoke-ShellCode Code Injection
Invoke-ReflectivePEInjection Process Injection
PowerView Active Directory Enumeration
SharpHound / BloodHound AD Attack Path Mapping
Rubeus Kerberos Ticket Manipulation
Out-Minidump LSASS Memory Dumping

Download Cradle Patterns

Pattern Example
Net.WebClient (New-Object Net.WebClient).DownloadString(...)
Invoke-WebRequest IWR -Uri http://... -OutFile ...
DownloadString $wc.DownloadString('http://...')
Start-BitsTransfer Start-BitsTransfer -Source http://...
Invoke-RestMethod IRM http://... | IEX

Obfuscation Indicators

Pattern Description
-EncodedCommand / -enc Base64-encoded PowerShell command
IEX / Invoke-Expression Dynamic execution of string content
[Convert]::FromBase64String Base64 decoding in script
-join [char[]] Character array concatenation obfuscation
.Replace() chaining String substitution for keyword evasion

python-evtx Library Usage

import Evtx.Evtx as evtx
from lxml import etree
 
with evtx.Evtx("PowerShell-Operational.evtx") as log:
    for record in log.records():
        xml = record.xml()
        root = etree.fromstring(xml.encode("utf-8"))
        # Extract EventID, EventData fields

CLI Usage

# Hunt for suspicious PowerShell in EVTX file
python agent.py --evtx /path/to/PowerShell-Operational.evtx
 
# Limit events parsed
python agent.py --evtx logs.evtx --max-events 5000
 
# Save report to JSON
python agent.py --evtx logs.evtx --output hunt_report.json

Group Policy Settings for Script Block Logging

Computer Configuration > Administrative Templates > Windows Components
  > Windows PowerShell > Turn on PowerShell Script Block Logging
    -> Enabled
    -> Log script block invocation start / stop events: Checked

External References

Scripts 1

agent.py8.6 KB
Display-only source. This catalog never executes bundled scripts.
#!/usr/bin/env python3
"""PowerShell Script Block Logging threat hunting agent."""

import json
import sys
import argparse
import base64
import re
from datetime import datetime
from collections import defaultdict

try:
    import Evtx.Evtx as evtx
    from lxml import etree
except ImportError:
    print("Install: pip install python-evtx lxml")
    sys.exit(1)

NS = {"e": "http://schemas.microsoft.com/win/2004/08/events/event"}

AMSI_INDICATORS = [
    "amsiutils", "amsiinitfailed", "amsicontext", "amsisession",
    "amsiinitialize", "amsi.dll", "amsiScanBuffer",
    "System.Management.Automation.AmsiUtils",
]

SUSPICIOUS_KEYWORDS = [
    "Invoke-Mimikatz", "Invoke-Kerberoast", "Invoke-ShellCode",
    "Invoke-ReflectivePEInjection", "Invoke-TokenManipulation",
    "Get-GPPPassword", "Get-Keystrokes", "Get-TimedScreenshot",
    "Out-Minidump", "Invoke-NinjaCopy", "Invoke-CredentialInjection",
    "Invoke-DllInjection", "Invoke-WMICommand", "PowerSploit",
    "Empire", "BloodHound", "Rubeus", "SharpHound",
    "Invoke-PSInject", "Invoke-RunAs", "PowerView",
]

DOWNLOAD_PATTERNS = [
    r"Net\.WebClient", r"Invoke-WebRequest", r"wget\s", r"curl\s",
    r"DownloadString", r"DownloadFile", r"DownloadData",
    r"Start-BitsTransfer", r"Invoke-RestMethod",
    r"New-Object\s+IO\.MemoryStream",
]

OBFUSCATION_PATTERNS = [
    r"-[Ee]nc(?:oded)?[Cc]ommand",
    r"\-e\s+[A-Za-z0-9+/=]{20,}",
    r"IEX\s*\(",
    r"Invoke-Expression",
    r"\[Convert\]::FromBase64String",
    r"\[System\.Text\.Encoding\]::",
    r"\.Replace\(['\"][^'\"]+['\"],\s*['\"][^'\"]+['\"]\)",
    r"-join\s*\[char\[\]\]",
    r"\$env:comspec",
]


def parse_evtx_4104(evtx_path, max_events=10000):
    """Parse Event 4104 script block logging entries from EVTX."""
    events = []
    count = 0
    with evtx.Evtx(evtx_path) as log:
        for record in log.records():
            if count >= max_events:
                break
            xml = record.xml()
            root = etree.fromstring(xml.encode("utf-8"))
            event_id_el = root.find(".//e:System/e:EventID", NS)
            if event_id_el is None or event_id_el.text != "4104":
                continue
            count += 1
            time_el = root.find(".//e:System/e:TimeCreated", NS)
            timestamp = time_el.get("SystemTime", "") if time_el is not None else ""
            data = {}
            for el in root.findall(".//e:EventData/e:Data", NS):
                name = el.get("Name", "")
                data[name] = el.text or ""
            events.append({
                "timestamp": timestamp,
                "script_block_id": data.get("ScriptBlockId", ""),
                "script_block_text": data.get("ScriptBlockText", ""),
                "message_number": data.get("MessageNumber", "1"),
                "message_total": data.get("MessageTotal", "1"),
                "path": data.get("Path", ""),
            })
    return events


def reassemble_script_blocks(events):
    """Reassemble multi-part script blocks by ScriptBlockId."""
    blocks = defaultdict(list)
    for ev in events:
        sb_id = ev.get("script_block_id", "")
        if sb_id:
            blocks[sb_id].append(ev)
    assembled = []
    for sb_id, parts in blocks.items():
        parts.sort(key=lambda x: int(x.get("message_number", "1")))
        full_text = "".join(p.get("script_block_text", "") for p in parts)
        assembled.append({
            "script_block_id": sb_id,
            "timestamp": parts[0].get("timestamp", ""),
            "path": parts[0].get("path", ""),
            "parts": len(parts),
            "full_text": full_text,
        })
    return assembled


def detect_amsi_bypass(script_text):
    """Check script text for AMSI bypass indicators."""
    findings = []
    lower = script_text.lower()
    for indicator in AMSI_INDICATORS:
        if indicator.lower() in lower:
            findings.append({"type": "amsi_bypass", "indicator": indicator})
    return findings


def detect_suspicious_keywords(script_text):
    """Check for known offensive tool keywords."""
    findings = []
    for kw in SUSPICIOUS_KEYWORDS:
        if kw.lower() in script_text.lower():
            findings.append({"type": "credential_or_offensive_tool", "keyword": kw})
    return findings


def detect_download_cradles(script_text):
    """Detect download cradle patterns in script text."""
    findings = []
    for pattern in DOWNLOAD_PATTERNS:
        if re.search(pattern, script_text, re.IGNORECASE):
            findings.append({"type": "download_cradle", "pattern": pattern})
    return findings


def detect_obfuscation(script_text):
    """Detect obfuscation and encoded command patterns."""
    findings = []
    for pattern in OBFUSCATION_PATTERNS:
        if re.search(pattern, script_text, re.IGNORECASE):
            findings.append({"type": "obfuscation", "pattern": pattern})
    b64_match = re.search(r"[A-Za-z0-9+/=]{40,}", script_text)
    if b64_match:
        try:
            decoded = base64.b64decode(b64_match.group()).decode("utf-16-le", errors="ignore")
            if any(c.isalpha() for c in decoded[:20]):
                findings.append({
                    "type": "encoded_payload",
                    "decoded_preview": decoded[:200],
                })
        except Exception:
            pass
    return findings


def hunt_scripts(assembled_blocks):
    """Run all detection checks on assembled script blocks."""
    results = []
    for block in assembled_blocks:
        text = block.get("full_text", "")
        if not text.strip():
            continue
        findings = []
        findings.extend(detect_amsi_bypass(text))
        findings.extend(detect_suspicious_keywords(text))
        findings.extend(detect_download_cradles(text))
        findings.extend(detect_obfuscation(text))
        if findings:
            results.append({
                "script_block_id": block["script_block_id"],
                "timestamp": block["timestamp"],
                "path": block["path"],
                "text_preview": text[:300],
                "findings": findings,
                "severity": "high" if any(
                    f["type"] in ("amsi_bypass", "credential_or_offensive_tool")
                    for f in findings
                ) else "medium",
            })
    return results


def run_audit(args):
    """Execute PowerShell script block hunting."""
    print(f"\n{'='*60}")
    print(f"  POWERSHELL SCRIPT BLOCK HUNTING")
    print(f"  Generated: {datetime.utcnow().isoformat()} UTC")
    print(f"{'='*60}\n")

    report = {}
    events = parse_evtx_4104(args.evtx, args.max_events)
    report["total_4104_events"] = len(events)
    print(f"Parsed {len(events)} Event 4104 records\n")

    blocks = reassemble_script_blocks(events)
    report["unique_script_blocks"] = len(blocks)
    print(f"Reassembled {len(blocks)} unique script blocks\n")

    results = hunt_scripts(blocks)
    report["suspicious_blocks"] = len(results)
    report["findings"] = results

    amsi = sum(1 for r in results if any(f["type"] == "amsi_bypass" for f in r["findings"]))
    cred = sum(1 for r in results if any(f["type"] == "credential_or_offensive_tool" for f in r["findings"]))
    dl = sum(1 for r in results if any(f["type"] == "download_cradle" for f in r["findings"]))
    obf = sum(1 for r in results if any(f["type"] == "obfuscation" for f in r["findings"]))
    report["summary"] = {
        "amsi_bypass_attempts": amsi,
        "credential_access": cred,
        "download_cradles": dl,
        "obfuscation_detected": obf,
    }

    print(f"--- HUNT RESULTS ---")
    print(f"  AMSI bypass attempts: {amsi}")
    print(f"  Credential/offensive tools: {cred}")
    print(f"  Download cradles: {dl}")
    print(f"  Obfuscation detected: {obf}")
    print(f"\n--- HIGH SEVERITY ---")
    for r in results[:15]:
        if r["severity"] == "high":
            print(f"  [{r['timestamp']}] {r['script_block_id']}")
            for f in r["findings"]:
                print(f"    {f['type']}: {f.get('keyword', f.get('indicator', ''))}")

    return report


def main():
    parser = argparse.ArgumentParser(description="PowerShell Script Block Hunting Agent")
    parser.add_argument("--evtx", required=True,
                        help="Path to PowerShell Operational .evtx file")
    parser.add_argument("--max-events", type=int, default=10000,
                        help="Max events to parse (default: 10000)")
    parser.add_argument("--output", help="Save report to JSON file")
    args = parser.parse_args()

    report = run_audit(args)
    if args.output:
        with open(args.output, "w") as f:
            json.dump(report, f, indent=2, default=str)
        print(f"\n[+] Report saved to {args.output}")


if __name__ == "__main__":
    main()
Keep exploring