PowerShell Script Block Logging with Event ID 4104

Most PowerShell-based attacks rely on the same trick: pass a Base64-encoded command, a string concatenation, or a script downloaded at runtime, and hope nothing reads what actually executed. PowerShell Script Block Logging 4104 defeats that assumption by logging the script source after the parser has resolved encoding, concatenation, and variable substitution. This post is the enable-tune-hunt walkthrough for getting 4104 useful in a real environment.

Key Takeaways

  • Event ID 4104 in the Microsoft-Windows-PowerShell/Operational channel logs every script block PowerShell compiles — including the deobfuscated content of Base64 and concatenated strings.
  • 4104 is off by default. Enable it via Group Policy or a one-line registry write; the corresponding registry key is the canonical reference.
  • The default channel size overflows quickly on busy hosts. Raise Microsoft-Windows-PowerShell/Operational to 1 GB at minimum and forward to a central collector.
  • Hunt for the few high-signal patterns first: -EncodedCommand, FromBase64String, DownloadString, and reflective Assembly.Load calls. They produce one or two hits per fleet per day and are almost always worth reading.
  • 4104 pairs with Constrained Language Mode and AMSI — the event sees the code; the other two stop or scan it. Run all three.

Environment

  • Windows 10/11 and Windows Server 2019/2022 endpoints with PowerShell 5.1 or later (5.1 is the minimum that supports script block logging).
  • Group Policy or Intune deployment channel to push the registry key fleet-wide.
  • Windows Event Forwarding or a SIEM agent to ship the PowerShell Operational channel off-host. See our WEF setup post.
  • PowerShell 5.1 or 7.4 on the collector for hunting queries.

The Problem

PowerShell is the default execution environment for both legitimate administration and a large share of post-exploitation tooling on Windows. Empire, PowerSploit, Covenant, and most commodity loaders all reach for PowerShell because it is signed, present on every host, and trusted by application control. The mitigations Microsoft added in PowerShell 5.0 — script block logging, AMSI, Constrained Language Mode, transcription — are what make PowerShell defensible. Of those, 4104 is the one that produces the analyst-readable artefact: the actual code that ran, after the parser unrolled everything.

The wrinkle is that 4104 is off out of the box, and even when it is on, the channel size is too small to be useful on a busy host. A single web shell that loops every five seconds will fill the default 15 MB log in an afternoon. Detection engineering against 4104 starts with the operational work of enabling it correctly, forwarding it, and writing queries that pick out the few patterns worth alerting on.

The Solution

Step 1 — Enable script block logging

The supported path is Group Policy: Computer Configuration → Administrative Templates → Windows Components → Windows PowerShell → Turn on PowerShell Script Block Logging → Enabled. The "Log script block invocation start / stop events" checkbox underneath generates additional 4105/4106 events for every block — useful for execution chaining, noisy for everything else.

The registry equivalent is one key, scriptable for non-domain endpoints:

# Enable on a single host
$path = 'HKLM:\Software\Policies\Microsoft\Windows\PowerShell\ScriptBlockLogging'
New-Item -Path $path -Force | Out-Null
Set-ItemProperty -Path $path -Name 'EnableScriptBlockLogging' -Value 1 -Type DWord

Microsoft's authoritative reference for the logging settings is in the about_Logging_Windows documentation. The setting takes effect on the next PowerShell session — running sessions are not retroactively instrumented.

Step 2 — Size the channel and add transcription

Before traffic builds up, raise the channel size on every endpoint:

wevtutil sl 'Microsoft-Windows-PowerShell/Operational' /ms:1073741824   # 1 GB

For high-value hosts (jump boxes, admin workstations, build servers), enable transcription as well — script block logging captures what was compiled, transcription captures the full input and output as a text file:

$tx = 'HKLM:\Software\Policies\Microsoft\Windows\PowerShell\Transcription'
New-Item -Path $tx -Force | Out-Null
Set-ItemProperty -Path $tx -Name 'EnableTranscripting'   -Value 1 -Type DWord
Set-ItemProperty -Path $tx -Name 'EnableInvocationHeader' -Value 1 -Type DWord
Set-ItemProperty -Path $tx -Name 'OutputDirectory'       -Value '\\fileserver\pstx$\%COMPUTERNAME%' -Type String

Point transcripts to a write-only network share that the host can append to but not read back. That stops a compromised endpoint from tampering with its own transcript history while keeping the data centralised.

Step 3 — Where the deobfuscated text actually lives

Large script blocks split across multiple 4104 events, with two fields used to stitch them back together: MessageNumber and MessageTotal. The deobfuscated source is in the event message body, not in a separate property. The crucial point is that PowerShell logs the script after its parser has processed the input — passing -EncodedCommand <Base64> produces a 4104 containing the decoded UTF-16 source, not the Base64 string the operator typed.

String concatenation that survives the parser (assembling a command from variables, building method names character by character) is still visible — the 4104 logs the resulting compiled block, which contains the concatenated literals as written. Obfuscation that defeats 4104 has to defeat compilation itself, which leaves less room than most operators realise.

Step 4 — Hunt encoded payloads and download cradles

The single highest-signal query against 4104 is a regex over the message body for known loader strings:

# Encoded-or-downloaded payloads in the last 24h
Get-WinEvent -FilterHashtable @{
    LogName   = 'ForwardedEvents'
    ProviderName = 'Microsoft-Windows-PowerShell'
    Id        = 4104
    StartTime = (Get-Date).AddDays(-1)
} -ErrorAction SilentlyContinue |
    Where-Object {
        $_.Message -match '(?i)(\-enc(odedcommand)?|frombase64string|downloadstring|downloadfile|invoke-webrequest\s+[^|]+\.(ps1|exe|dll))'
    } |
    Select-Object TimeCreated, MachineName,
                  @{Name='User';    Expression={ $_.UserId }},
                  @{Name='Snippet'; Expression={ ($_.Message -split "`n")[0..3] -join ' / ' }}

This will catch the obvious cases. A typical environment produces a handful of hits per day — almost all of them either legitimate admin automation (treat as a tuning opportunity, allowlist by signed cert or known path) or genuinely worth investigating.

Step 5 — Hunt reflective .NET loading

A more advanced loader pattern: load a .NET assembly from a byte array in memory, never touching disk. Cobalt Strike's PowerShell stager, plenty of public C2 frameworks, and a fair number of red-team tools use it:

Get-WinEvent -FilterHashtable @{
    LogName   = 'ForwardedEvents'
    ProviderName = 'Microsoft-Windows-PowerShell'
    Id        = 4104
    StartTime = (Get-Date).AddDays(-1)
} -ErrorAction SilentlyContinue |
    Where-Object {
        $_.Message -match '\[Reflection\.Assembly\]::Load\(' -or
        $_.Message -match 'System\.Reflection\.AssemblyName' -or
        $_.Message -match 'GetDelegateForFunctionPointer'
    } |
    Select-Object TimeCreated, MachineName, Id,
                  @{Name='Snippet'; Expression={ ($_.Message -split "`n")[0..5] -join ' / ' }}

Reflective loading has almost no legitimate use case in modern administration — virtually every hit is worth a closer look at the host. Pair with Sysmon event 10 (process access) against lsass.exe from the same host to chain credential-access tooling.

Step 6 — Watch for AMSI tampering

AMSI (Antimalware Scan Interface) is the kernel-mode bridge that lets antivirus inspect PowerShell script content at parse time. Operators routinely try to break AMSI by patching amsi.dll in memory or setting amsiInitFailed via reflection. The bypass code itself is a 4104 event:

Get-WinEvent -FilterHashtable @{
    LogName   = 'ForwardedEvents'
    ProviderName = 'Microsoft-Windows-PowerShell'
    Id        = 4104
    StartTime = (Get-Date).AddDays(-7)
} -ErrorAction SilentlyContinue |
    Where-Object {
        $_.Message -match '(?i)amsiInitFailed|amsi\.dll|AmsiUtils|AmsiScanBuffer'
    } |
    Select-Object TimeCreated, MachineName,
                  @{Name='Snippet'; Expression={ ($_.Message -split "`n")[0..5] -join ' / ' }}

Even when the bypass succeeds against AMSI, it cannot prevent the 4104 that logged the bypass code itself — the event is written by PowerShell's logging subsystem before AMSI is invoked. The implication is that 4104 catches the bypass attempt even on the operations where AMSI is the thing being bypassed.

Frequently Asked Questions

Does script block logging slow PowerShell down?

In benchmarks on modern hardware, the overhead is in the low single digits of percent for typical workloads. Heavy automation that emits very large script blocks (multi-megabyte modules) sees more measurable impact because the channel write is synchronous. For everything else — interactive admin, scheduled tasks, normal scripting — the cost is not user-visible.

Will obfuscation defeat 4104?

Not the common kinds. Base64 encoding, character substitution, and string concatenation are resolved by the parser before logging, so the 4104 contains the recovered source. Obfuscation that survives is the rarer kind that builds the eventual code through layered Invoke-Expression chains or runtime AST manipulation, and even then each layer produces its own 4104. The forensic question shifts from "what did this run" to "which 4104 has the final payload."

What is the relationship between 4103 and 4104?

4103 is module logging — parameters and module member calls. 4104 is script block logging — the actual code text. 4103 tells you that Invoke-WebRequest was called with a particular URL parameter; 4104 tells you the full surrounding script. Both are useful; 4104 is the higher-signal of the two for hunting.

Why are some 4104 events flagged as Warning level?

PowerShell promotes a 4104 to Warning when the parser detects content matching its built-in suspicious-strings list (encoded commands, known reflection patterns, AMSI bypass strings). Filtering on LevelDisplayName = 'Warning' is a cheap pre-filter that surfaces the highest-value events without any custom regex.

Should I also enable transcription on every endpoint?

Workstation-wide transcription generates a significant volume of small text files and a corresponding storage and retention cost. The pragmatic split is to enable transcription on jump boxes, admin workstations, and high-value servers — places where every interactive session is worth keeping — and rely on 4104 alone for the broader fleet. Transcription pays off most when an analyst needs to read the exact output an operator saw, which is rare outside incident response.

Conclusion

PowerShell Script Block Logging is one of the cheapest, most analyst-friendly Windows detection events available — and one of the most commonly left disabled. Enable 4104 via Group Policy, raise the channel size, forward it off-host, and run the four or five regex queries above against the centralised stream. The signal-to-noise ratio is high enough that the alerts produced are mostly worth reading.

Combined with audit policy, Windows Event Forwarding, and Sysmon, 4104 closes the last big visibility gap in the standard Windows detection stack. Attackers who rely on PowerShell are loud against it; the work is making sure the channel is on, sized, and shipped.

Related Posts

Detecting Kerberoasting with Windows Event ID 4769

Kerberoasting (MITRE ATT&CK T1558.003) is one of the few credential-access techniques that produces a clean, on-prem audit signal — provided the right event is enabled and the right field is read. Detecting Kerberoasting with Event ID 4769 comes down to two things: alerting on RC4-HMAC service ticket requests in an environment that should be running AES, and watching for bursts of ticket requests against many SPNs from a single account. This post is the detection and hardening pair we use on the domain controllers we monitor.

Key Takeaways

  • Event ID 4769 on domain controllers records every Kerberos service ticket request. The Ticket Encryption Type field is the primary detection signal — 0x17 means RC4-HMAC, which is what offline cracking tools require.
  • Any modern Active Directory environment should rarely see RC4 service tickets. Treat 0x17 against domain user SPNs as anomalous until proven legitimate.
  • A burst of 4769s — one user requesting tickets for many distinct SPNs in a short window — is the classic Kerberoasting pattern, with or without RC4.
  • Hardening beats detection: set msDS-SupportedEncryptionTypes to AES-only on service accounts, migrate to Group Managed Service Accounts (gMSAs), and deploy a honey SPN for high-fidelity alerting.
  • 4769 is logged on the issuing domain controller, not the client. Centralise the Security log from every DC; one DC's events are not enough.

Environment

  • Active Directory domain at Windows Server 2016 functional level or higher.
  • Windows Server 2019/2022 domain controllers with Advanced Audit Policy applied via Group Policy.
  • 4769 events forwarded from every DC via Windows Event Forwarding or a SIEM agent.
  • PowerShell 5.1 or 7.4 for ad-hoc analysis on the collector.
  • RSAT Active Directory tooling for service-account hardening tasks.

The Problem

Kerberoasting works because Kerberos is doing exactly what it was designed to do. Any authenticated domain user can request a service ticket (TGS) for any account that has a Service Principal Name (SPN). The TGS is encrypted with a key derived from the target account's password. Older or misconfigured accounts produce RC4-HMAC tickets, which can be cracked offline at billions of guesses per second on a modern GPU. AES-encrypted tickets are computationally infeasible to crack at the same speed, which is why attackers explicitly request RC4 even on AES-capable accounts when they can.

The detection challenge is volume. 4769 fires for every service ticket request in the domain — Outlook to Exchange, SCCM to its database, end-user RDP, every internal web app. A single DC issues thousands of 4769s per minute. The trick is filtering down to the small number of requests that have the shape of an attack.

The Solution

Step 1 — Enable Kerberos service ticket auditing

Under Advanced Audit Policy → Account Logon, enable both Success and Failure for Audit Kerberos Service Ticket Operations. Apply via the Default Domain Controllers Policy so every DC inherits the same configuration:

# Verify on a DC
auditpol /get /subcategory:"Kerberos Service Ticket Operations"

Without this subcategory enabled, 4769 will never fire and the rest of this post is moot. Confirm at least one DC is logging the events before scaling out the detection.

Step 2 — Anatomy of a 4769 event

The fields that matter for detection:

  • Account Name — the user requesting the ticket. Will appear as USERNAME@DOMAIN.LOCAL.
  • Service Name — the SPN being requested. For Kerberoasting, this will be a domain user account name (not a computer or krbtgt).
  • Ticket Options — a flags field. 0x40810000 is normal; 0x40810010 often indicates ticket re-use.
  • Ticket Encryption Type — the heart of the detection. Common values: 0x12 (AES256-CTS-HMAC-SHA1-96), 0x11 (AES128), 0x17 (RC4-HMAC), 0x18 (RC4-HMAC-EXP).
  • Client Address — the source IP of the requester. Useful for narrowing the actor.
  • Failure Code0x0 for successful issuance; non-zero for errors.

Step 3 — Alert on RC4 service tickets

Modern domain members negotiate AES by default when the target account supports it. RC4 service tickets in an AES-capable environment fall into a few legitimate buckets — pre-Windows-Server-2008 trusts, accounts with msDS-SupportedEncryptionTypes unset or explicitly RC4 — and one illegitimate bucket: Kerberoasting tools forcing the encryption type down to make the ticket crackable.

# Surface RC4 service tickets issued in the last 24h, excluding machine accounts
Get-WinEvent -FilterHashtable @{
    LogName   = 'ForwardedEvents'
    Id        = 4769
    StartTime = (Get-Date).AddDays(-1)
} -ErrorAction SilentlyContinue |
    Where-Object {
        ($_.Properties[5].Value -eq '0x17' -or $_.Properties[5].Value -eq '0x18') -and
        ($_.Properties[2].Value -notmatch '\$$')
    } |
    Select-Object TimeCreated, MachineName,
                  @{Name='User';     Expression={ $_.Properties[0].Value }},
                  @{Name='Service';  Expression={ $_.Properties[2].Value }},
                  @{Name='ClientIP'; Expression={ $_.Properties[6].Value }},
                  @{Name='EncType';  Expression={ $_.Properties[5].Value }}

The -notmatch '\$$' filter drops machine accounts (Kerberoasting targets user accounts with SPNs, not computer accounts). Whatever survives this query should be a short list — investigate every entry.

Step 4 — Alert on ticket-request bursts

Attackers that cannot force RC4 will still leave a behavioural fingerprint: one principal requesting tickets for an unusually large number of distinct SPNs in a short window. The query is shape-based and works regardless of encryption type:

# Same user requesting tickets for many SPNs in an hour
Get-WinEvent -FilterHashtable @{
    LogName   = 'ForwardedEvents'
    Id        = 4769
    StartTime = (Get-Date).AddHours(-1)
} -ErrorAction SilentlyContinue |
    Where-Object { $_.Properties[2].Value -notmatch '\$$' -and
                   $_.Properties[2].Value -notmatch 'krbtgt' } |
    Group-Object { $_.Properties[0].Value } |
    ForEach-Object {
        [pscustomobject]@{
            User           = $_.Name
            DistinctSPNs   = ($_.Group | Select-Object -ExpandProperty Properties |
                              ForEach-Object { $_[2].Value } | Sort-Object -Unique).Count
            Total          = $_.Count
        }
    } |
    Where-Object DistinctSPNs -gt 20 |
    Sort-Object DistinctSPNs -Descending

Tune the threshold to the environment — 20 distinct SPNs per hour from one user is loud in most domains and quiet in a few. Legitimate hits are typically service accounts running discovery tooling (vulnerability scanners, asset inventories, monitoring agents). Allowlist by account name once those are identified.

Step 5 — Deploy a honey SPN

The highest-fidelity Kerberoasting detection is a decoy. Create a domain user account that no legitimate service ever talks to, register a plausible SPN against it, and alert on every 4769 issued for that SPN. Two false-positive sources to plan around: AD discovery scans by red-team tooling and the occasional curious admin running Get-ADUser -Filter * -Properties servicePrincipalName.

# Create the decoy
$pw = -join ((33..126) | Get-Random -Count 64 | ForEach-Object { [char]$_ })
New-ADUser -Name 'svc-backup-sql' `
           -SamAccountName 'svc-backup-sql' `
           -AccountPassword (ConvertTo-SecureString $pw -AsPlainText -Force) `
           -Enabled $true `
           -Description 'Service account — do not modify'

setspn -S MSSQLSvc/backup-sql.example.local:1433 svc-backup-sql

# Disable interactive logon and pre-set a long, random password
Set-ADUser -Identity svc-backup-sql -CannotChangePassword $true -PasswordNeverExpires $true

Then create a SIEM rule that alerts on Service Name = MSSQLSvc/backup-sql.example.local:1433 in any 4769 event. The account is never used by anything legitimate, so every match is a true positive.

Step 6 — Harden service accounts

Detection is reactive; the hardening below removes the technique outright for any account it covers:

  • Force AES on service accounts. Set msDS-SupportedEncryptionTypes to 0x18 (AES128 + AES256). Tickets issued to those accounts will no longer be RC4 regardless of what the client requests.
  • Use Group Managed Service Accounts. A gMSA has a 240-character password that AD rotates automatically every 30 days. The password is never typed, never stored, and cannot be cracked at any practical speed. Migrate any service that supports gMSAs.
  • Long passwords on remaining accounts. For services that do not support gMSAs, set a 25+ character random password. RC4 cracking against a 25-character password is computationally infeasible regardless of GPU budget.
  • Remove unused SPNs. SPNs on accounts that no longer host the service are pure attack surface. Audit servicePrincipalName against actual running services annually.
# Set AES-only on a service account
Set-ADUser -Identity svc-sql-prod -Replace @{ 'msDS-SupportedEncryptionTypes' = 24 }

# List all SPNs in the domain for review
Get-ADUser -Filter * -Properties servicePrincipalName |
    Where-Object servicePrincipalName |
    Select-Object SamAccountName, @{N='SPNs'; E={ $_.servicePrincipalName -join '; ' }}

Frequently Asked Questions

Why does Event ID 4769 fire so often even in a quiet domain?

4769 is the standard Kerberos service ticket flow — every domain client requests one for every service it talks to, then caches it for the ticket lifetime (10 hours by default). Outlook, file shares, SQL connections, RDP, and internal web apps all generate 4769s constantly. Volume is normal; the goal is filtering on encryption type and request shape, not on overall count.

Can I detect Kerberoasting without forwarding logs from every domain controller?

Not reliably. A client can request a service ticket from any DC the domain resolves; observing only one DC misses tickets issued by the others. For consistent coverage, forward the Security log from every DC to a single collector or SIEM and run detections against the merged stream.

What encryption type values should I expect in a healthy domain?

0x12 (AES256) is the modern default for tickets issued to AES-capable accounts. 0x11 (AES128) appears for older or differently configured accounts. 0x17 (RC4-HMAC) should be rare and should map to a known list of legacy accounts. 0x18 (RC4-HMAC-EXP) is exceptional and worth investigating wherever it shows up.

Do gMSAs eliminate Kerberoasting entirely?

For the accounts they cover, effectively yes. The 240-character random password is rotated automatically and cannot be brute-forced at any realistic speed. gMSAs do not retroactively protect accounts that still hold cracking-feasible passwords, so the migration is the work — the protection is automatic once it lands.

Will Kerberoasting still be detectable if the attacker uses AES?

The encryption-type signal goes away, but the behavioural signal does not. Tools that request tickets for every SPN they enumerate still produce the burst pattern in Step 4 — one principal asking for many distinct SPNs in a short window. The honey SPN in Step 5 also fires regardless of encryption type. Defense in depth matters here precisely because the easiest detection can be bypassed.

Conclusion

Kerberoasting is the rare offensive technique where the protocol-level signal is unambiguous if the right audit is on. Enable Kerberos Service Ticket Operations auditing, forward 4769s from every DC, alert on RC4 issuance to non-machine accounts, and add a honey SPN for high-fidelity coverage. Then do the unglamorous half of the work: AES-only encryption types, gMSAs where possible, and long random passwords on whatever remains.

The detections in this post are not novel — they are the same patterns the public detection-engineering community has published since 2016. What makes them effective is having them on, having them centralised, and having the hardening done so the alerts that fire actually mean something.

Related Posts

Sysmon Configuration for Windows Security Monitoring

Native Windows auditing covers a surprising amount of ground, but it has known gaps: no file hashes on process creation, no outbound network connections, no LSASS access telemetry, and no built-in DNS query log. Sysmon configuration for security monitoring closes most of those gaps and is the single highest-value addition to a Windows endpoint detection stack after audit policy itself. This post is the deployment we use internally — install, baseline config, scaled rollout, and the event IDs that actually carry signal.

Key Takeaways

  • Sysmon is a free Sysinternals tool that augments the Windows event log with process, network, registry, file, image-load, and DNS telemetry — none of which native auditing covers well out of the box.
  • The configuration file is what matters. An empty Sysmon install logs almost nothing useful; a tuned config (SwiftOnSecurity or Olaf Hartong's modular project) is the sane starting point.
  • Deploy via a Group Policy scheduled task or Intune script so updates flow through the same channel as the rest of the fleet's tooling.
  • Event IDs 1 (process), 3 (network), 10 (process access), 11 (file create), and 22 (DNS query) cover the majority of high-value detection use cases.
  • Forward the Microsoft-Windows-Sysmon/Operational channel to a Windows Event Collector or SIEM. Sysmon on its own is per-host; centralised logs are what make it useful for a fleet.

Environment

  • Windows 10/11 and Windows Server 2019/2022 endpoints.
  • Sysmon v15 for Windows (the current major version as of writing — Sysmon for Linux is a separate project with a different event schema).
  • An existing Group Policy or Intune deployment channel to push binaries and config updates.
  • A Windows Event Collector (WEC) or SIEM to receive forwarded Sysmon events. See our Windows Event Forwarding setup guide for the collector side.
  • PowerShell 5.1 or 7.4 for hunting queries on the collector.

The Problem

Built-in Windows auditing is good at telling you that a process started (4688), that an account logged on (4624), or that an AD object changed (5136). It is less good at the things modern detection actually depends on: the SHA-256 of the binary that ran, the IP it then connected to, the DLL it loaded that did not match a Microsoft signature, the handle it requested on the LSASS process. Some of that is available with extra audit subcategories, command-line inclusion settings, or Object Access auditing, but the coverage is uneven and the events are designed for compliance more than detection.

Sysmon was written by Sysinternals (Mark Russinovich and Thomas Garnier) to fill those gaps. It installs as a kernel driver plus a user-mode service, hooks the events of interest at the source, and writes them to a dedicated event channel. Crucially, it is free, signed by Microsoft, supported on every modern Windows version, and produces telemetry that maps cleanly to MITRE ATT&CK techniques. The trade-off is that it ships with no useful configuration — running sysmon.exe -i with no config file logs almost nothing of value. The configuration file is the entire product.

The Solution

Step 1 — Install Sysmon on a test endpoint

Download Sysmon from the Sysinternals page on Microsoft Learn. Verify the signature, then install with a configuration file in one step:

# Run elevated
sysmon.exe -accepteula -i sysmonconfig.xml

# Confirm install
Get-Service Sysmon* | Format-Table Name, Status, StartType

The installer registers the driver, starts the service, and begins writing to the Microsoft-Windows-Sysmon/Operational channel. To update the config later, run sysmon.exe -c sysmonconfig.xml — no reinstall, no reboot. The channel is small by default; bump it before traffic builds up:

wevtutil sl Microsoft-Windows-Sysmon/Operational /ms:1073741824   # 1 GB

Step 2 — Pick a configuration baseline

Two community baselines cover virtually every production Sysmon deployment in the wild:

  • SwiftOnSecurity/sysmon-config — a single curated XML, heavily commented, conservative on volume. The right starting point for most environments.
  • olafhartong/sysmon-modular — a modular framework where rules live in separate files per ATT&CK technique. More work to assemble, but cleaner to maintain and easier to map to detection coverage.

Both projects are open source and well-maintained. We use SwiftOnSecurity's baseline as the floor and pull additional modules from sysmon-modular for areas the baseline omits — most notably credential-access (LSASS handle requests) and lateral-movement (named pipe creation) coverage. The rule of thumb is to enable everything except event ID 7 (image load) initially, then turn 7 on for a defined set of high-value processes once log volume is understood.

Step 3 — Deploy and update across the fleet

At scale, copy the Sysmon binary and config to a known location and trigger install or reconfigure on every endpoint. A Group Policy scheduled task is the simplest mechanism that does not require additional tooling:

# Idempotent deployment script
$bin    = '\\dfs\sysmon\Sysmon64.exe'
$config = '\\dfs\sysmon\sysmonconfig.xml'

if (Get-Service Sysmon* -ErrorAction SilentlyContinue) {
    & $bin -c $config
} else {
    & $bin -accepteula -i $config
}

Wrap that in a scheduled task that runs daily as SYSTEM, target it via GPO at every domain-joined endpoint, and config changes propagate within 24 hours of being copied to the share. Intune Win32 app deployment works the same way for cloud-managed endpoints. Whichever channel is used, the binary and configuration both need version pinning — Sysmon's schema occasionally adds new fields, and a config authored against schema 4.90 will fail to load on a host running an older binary.

Step 4 — The Sysmon event IDs that carry signal

Sysmon emits 27 distinct event IDs at the time of writing. A much smaller subset is where most detections live:

  • 1 — Process creation. Includes the SHA-256 hash, parent process, full command line, and integrity level. The single most valuable Sysmon event.
  • 3 — Network connection. Outbound TCP/UDP including destination IP, port, and the process that initiated it.
  • 7 — Image loaded (DLL load). High volume; enable selectively for processes like lsass.exe, winlogon.exe, and Office binaries.
  • 8 — CreateRemoteThread. Classic process injection primitive.
  • 10 — Process access. The GrantedAccess field is where LSASS credential dumping shows up (values around 0x1010 or 0x1410).
  • 11 — File create. Useful for staging detections (executables written to %TEMP%, %APPDATA%, or web-shell paths).
  • 12 / 13 / 14 — Registry events. Pair with autorun and persistence keys.
  • 22 — DNS query. Every resolution made by every process — the single best telemetry source for C2 callback detection.
  • 25 — Process tampering. Process hollowing and image replacement.

Step 5 — Forward Sysmon to the collector

Add Microsoft-Windows-Sysmon/Operational to the subscription XML on the Windows Event Collector. The simplest path is a second Select path inside an existing baseline subscription:

<Select Path="Microsoft-Windows-Sysmon/Operational">
  *[System[(EventID=1 or EventID=3 or EventID=7 or EventID=8 or
            EventID=10 or EventID=11 or EventID=22 or EventID=25)]]
</Select>

Sysmon volume is significant — a single workstation can push 500 to 2,000 events per minute depending on which IDs are enabled. Plan for a separate forwarded log if the collector is serving more than a handful of endpoints; the WEF setup post covers custom event channels for exactly this case.

Step 6 — Hunting queries

A few queries that pay off the first time they run against forwarded Sysmon data:

# Event 10 — handle requests on lsass.exe with credential-dumping access masks
Get-WinEvent -FilterHashtable @{
    LogName = 'ForwardedEvents'
    ProviderName = 'Microsoft-Windows-Sysmon'
    Id = 10
    StartTime = (Get-Date).AddDays(-1)
} -ErrorAction SilentlyContinue |
    Where-Object { $_.Message -match 'TargetImage:.*lsass\.exe' -and
                   $_.Message -match 'GrantedAccess:\s*0x1(0|4)10' } |
    Select-Object TimeCreated, MachineName,
                  @{Name='SourceImage'; Expression={
                      ($_.Message -split "`n" | Where-Object { $_ -match 'SourceImage' }) -replace '.*: '
                  }}

# Event 22 — DNS queries from processes that should not be resolving names
Get-WinEvent -FilterHashtable @{
    LogName = 'ForwardedEvents'
    ProviderName = 'Microsoft-Windows-Sysmon'
    Id = 22
    StartTime = (Get-Date).AddHours(-1)
} -ErrorAction SilentlyContinue |
    Where-Object { $_.Message -match 'Image:.*\\(certutil|bitsadmin|powershell|regsvr32)\.exe' } |
    Select-Object TimeCreated, MachineName,
                  @{Name='Query'; Expression={
                      ($_.Message -split "`n" | Where-Object { $_ -match 'QueryName' }) -replace '.*: '
                  }}

These are the kinds of queries that produce one or two hits per fleet per day and almost always warrant attention when they fire.

Frequently Asked Questions

Do I still need native Windows auditing if I have Sysmon?

Yes. Sysmon does not replace the Security log — it sits next to it. Authentication events (4624, 4625), account management (4720, 4728), AD object changes (5136), and audit policy changes (1102, 4719) all live in the Security channel and have no Sysmon equivalent. Sysmon adds process, network, image, and DNS telemetry; the Security log keeps everything else. Run both, forward both.

Will Sysmon affect endpoint performance?

On modern hardware, the user-visible impact is minimal — Sysmon hooks the events in kernel mode and writes to a dedicated channel, so most cost is in disk I/O on the event log itself. The two configuration choices that drive measurable CPU and disk use are event ID 7 (image load — every DLL load on every process) and overly broad rules on event ID 13 (registry value set). Both can be tuned with exclude rules in the config.

How is Sysmon different from Microsoft Defender for Endpoint?

Defender for Endpoint is a commercial EDR with its own telemetry pipeline, behavioural detections, and response capability. Sysmon is a free telemetry source that writes to the Windows event log; the analytics happen wherever you ship the log. Most environments running Defender for Endpoint do not also need Sysmon, since Defender collects similar telemetry through its agent. Sysmon is the right answer when the SIEM is something other than Defender XDR — Sentinel without P2, Splunk, Elastic, or a homegrown collector.

How should I update the Sysmon configuration across the fleet?

Treat the config like any other production artefact — version-control the XML, copy it to a known share or container, and run sysmon.exe -c on every endpoint on a schedule. The command is idempotent: applying the same config twice is a no-op. The Group Policy scheduled task pattern in Step 3 handles this without additional infrastructure.

Is Sysmon event ID 7 (image load) worth the volume?

Selectively, yes — but not for every process. Enabling event 7 globally adds hundreds of events per second per endpoint. The useful pattern is to include event 7 only for the small set of processes that matter for credential-access and persistence: lsass.exe, winlogon.exe, services.exe, and any Office or browser binary that hosts macros or extensions. Both SwiftOnSecurity and sysmon-modular ship include-by-default rules along these lines.

Conclusion

Sysmon is rare among free security tools in that the deployment effort is small and the detection lift is large. The hard parts are picking a configuration baseline and getting the events centralised — both are solved problems with mature open-source projects. Once those two pieces are in place, the event IDs above expose roughly the same telemetry an expensive EDR collects, in a format any SIEM can ingest.

Combined with native audit policy and Windows Event Forwarding, Sysmon gives a Windows environment detection coverage that is genuinely useful — not the box-checking kind, but the kind that catches things.

Related Posts