Aaron SawitSecurity engineer · Singapore

Detections · Sigma · MITRE ATT&CK

Rules that are tested, not just written

A rule that parses is not a rule that works. Each of these has events it must match and events it must not, checked by a small harness, validated with sigma check, and converted to Splunk SPL and Elastic Lucene. 13 fixture checks, all passing in CI. The repository is on GitHub.

System DLL loaded from a user-writable path

T1574.001level: highSysmon event 7

DLL search-order hijacking. Written from a Sysmon lab where a copied calc.exe loaded a planted WININET.dll. Where it came from

Sigma rule
title: System DLL Loaded From a User-Writable Path
id: 6f2b1d0a-7c1e-4a53-9a57-2f1d3c8e4b10
status: experimental
description: >
  Detects a well-known Windows system DLL being loaded from a user-writable directory instead of
  System32 or SysWOW64. This is the footprint of DLL search-order hijacking, for example a copy of
  calc.exe on the Desktop loading a planted WININET.dll that sits beside it.
references:
  - https://attack.mitre.org/techniques/T1574/001/
  - https://aaronsawit.com/writing/hunting-dll-hijacking-with-sysmon/
author: Aaron Sawit
date: 2026-09-20
tags:
  - attack.stealth
  - attack.execution
  - attack.t1574.001
logsource:
  product: windows
  category: image_load
detection:
  selection_dll:
    ImageLoaded|endswith:
      - '\wininet.dll'
      - '\version.dll'
      - '\dbghelp.dll'
      - '\winhttp.dll'
      - '\cryptbase.dll'
      - '\userenv.dll'
  selection_path:
    ImageLoaded|contains:
      - '\Users\'
      - '\ProgramData\'
      - '\Windows\Temp\'
      - '\Downloads\'
  filter_signed_microsoft:
    Signed: 'true'
    Signature: 'Microsoft Windows'
  condition: selection_dll and selection_path and not filter_signed_microsoft
falsepositives:
  - Portable applications that ship their own copy of a redistributable DLL. Tune by Image path.
level: high
Splunk SPL, generated
ImageLoaded IN ("*\\wininet.dll", "*\\version.dll", "*\\dbghelp.dll", "*\\winhttp.dll", "*\\cryptbase.dll", "*\\userenv.dll") ImageLoaded IN ("*\\Users\\*", "*\\ProgramData\\*", "*\\Windows\\Temp\\*", "*\\Downloads\\*") NOT (Signed="true" Signature="Microsoft Windows")
Elastic Lucene, generated
(ImageLoaded:(*\\wininet.dll OR *\\version.dll OR *\\dbghelp.dll OR *\\winhttp.dll OR *\\cryptbase.dll OR *\\userenv.dll)) AND (ImageLoaded:(*\\Users\\* OR *\\ProgramData\\* OR *\\Windows\\Temp\\* OR *\\Downloads\\*)) AND (NOT (Signed:true AND Signature:"Microsoft Windows"))

.NET runtime loaded by a process that is not a .NET host

T1059.001T1055level: highSysmon event 7

Unmanaged PowerShell: the CLR injected into a native process so PowerShell runs with no powershell.exe in the process log. Where it came from

Sigma rule
title: .NET Runtime Loaded by a Process That Is Not a .NET Host
id: 0c7a9e52-3b64-4d0f-8a21-5e9d1f6b7c33
status: experimental
description: >
  Detects clr.dll or clrjit.dll being loaded by a native Windows binary that has no reason to host
  the .NET runtime. Injecting the CLR into another process is how "unmanaged PowerShell" runs
  PowerShell code without powershell.exe appearing in process logs.
references:
  - https://attack.mitre.org/techniques/T1059/001/
  - https://attack.mitre.org/techniques/T1055/
author: Aaron Sawit
date: 2026-09-20
tags:
  - attack.execution
  - attack.stealth
  - attack.t1059.001
  - attack.t1055
logsource:
  product: windows
  category: image_load
detection:
  selection:
    ImageLoaded|endswith:
      - '\clr.dll'
      - '\clrjit.dll'
    Image|endswith:
      - '\spoolsv.exe'
      - '\notepad.exe'
      - '\calc.exe'
      - '\rundll32.exe'
      - '\svchost.exe'
      - '\lsass.exe'
  condition: selection
falsepositives:
  - rundll32.exe legitimately hosting a managed component. Baseline first, then exclude by command line.
level: high
Splunk SPL, generated
ImageLoaded IN ("*\\clr.dll", "*\\clrjit.dll") Image IN ("*\\spoolsv.exe", "*\\notepad.exe", "*\\calc.exe", "*\\rundll32.exe", "*\\svchost.exe", "*\\lsass.exe")
Elastic Lucene, generated
(ImageLoaded:(*\\clr.dll OR *\\clrjit.dll)) AND (Image:(*\\spoolsv.exe OR *\\notepad.exe OR *\\calc.exe OR *\\rundll32.exe OR *\\svchost.exe OR *\\lsass.exe))

SSH password brute force from one source

T1110.001level: mediumsshd auth log

Ten failed passwords from one address in five minutes, as a Sigma correlation rule. Ported from my log triage tool. Where it came from

Sigma rule
title: SSH Failed Password
id: 9d3e5a71-42c8-4f6b-b0d9-7a1c2e3f4a55
status: experimental
description: Base event for the brute-force correlation below. A single failed SSH password is not an alert.
author: Aaron Sawit
date: 2026-09-20
tags:
  - attack.credential-access
  - attack.t1110.001
logsource:
  product: linux
  service: sshd
detection:
  selection:
    message|contains: 'Failed password for'
  condition: selection
level: informational
---
title: SSH Password Brute Force From One Source
id: 1b8f6c24-9e07-4a3d-8c52-6d4e5f7a8b66
status: experimental
description: >
  Ten or more failed SSH passwords from one source address within five minutes. Ported from the
  ssh brute-force rule in my blue-team-ai log triage tool.
references:
  - https://attack.mitre.org/techniques/T1110/001/
  - https://github.com/aaronsawit/blue-team-ai
author: Aaron Sawit
date: 2026-09-20
tags:
  - attack.credential-access
  - attack.t1110.001
correlation:
  type: event_count
  rules:
    - 9d3e5a71-42c8-4f6b-b0d9-7a1c2e3f4a55
  group-by:
    - src_ip
  timespan: 5m
  condition:
    gte: 10
falsepositives:
  - A misconfigured backup job or monitoring probe with a stale password.
level: medium
Splunk SPL, generated
message="*Failed password for*"

| bin _time span=5m
| stats count as event_count by _time src_ip

| search event_count >= 10

Client bypassing the filtering resolver with encrypted DNS

T1071.004T1572level: lowDNS query log

A device looks up a DNS-over-HTTPS or Private Relay bootstrap host, after which the resolver stops seeing it. Written after it happened on my own network. Where it came from

Sigma rule
title: Client Bypassing the Filtering Resolver With Encrypted DNS or a Relay
id: 4e1a7b93-5d20-4c8e-9f36-8b2c0d1e9f77
status: experimental
description: >
  A client on a network with a mandatory filtering resolver looks up a public DNS-over-HTTPS
  endpoint or an iCloud Private Relay bootstrap host. Once that lookup succeeds the client's
  remaining DNS leaves the network encrypted, and the resolver's logs and blocklists stop applying
  to it. Written after one phone on my own network went silent in the resolver log.
references:
  - https://attack.mitre.org/techniques/T1071/004/
  - https://attack.mitre.org/techniques/T1572/
  - https://developer.apple.com/support/prepare-your-network-for-icloud-private-relay/
  - https://aaronsawit.com/writing/iphone-bypassing-dns-filter-private-relay/
author: Aaron Sawit
date: 2026-09-20
tags:
  - attack.command-and-control
  - attack.t1071.004
  - attack.t1572
logsource:
  category: dns
detection:
  selection:
    query|endswith:
      - 'mask.icloud.com'
      - 'mask-h2.icloud.com'
      - 'dns.google'
      - 'cloudflare-dns.com'
      - 'one.one.one.one'
      - 'dns.quad9.net'
      - 'doh.opendns.com'
      - 'dns.nextdns.io'
  filter_resolver_itself:
    src_ip:
      - '127.0.0.1'
      - '172.20.0.2'
  condition: selection and not filter_resolver_itself
falsepositives:
  - Personal devices with Private Relay enabled. On a home or guest network this is a visibility signal, not an incident.
level: low
Splunk SPL, generated
query IN ("*mask.icloud.com", "*mask-h2.icloud.com", "*dns.google", "*cloudflare-dns.com", "*one.one.one.one", "*dns.quad9.net", "*doh.opendns.com", "*dns.nextdns.io") NOT (src_ip IN ("127.0.0.1", "172.20.0.2"))
Elastic Lucene, generated
(query:(*mask.icloud.com OR *mask\-h2.icloud.com OR *dns.google OR *cloudflare\-dns.com OR *one.one.one.one OR *dns.quad9.net OR *doh.opendns.com OR *dns.nextdns.io)) AND (NOT (src_ip:(127.0.0.1 OR 172.20.0.2)))