Detection Engineering: YARA, Sigma & Behavioral Rules
Chapters 2 through 5 walked one sample from a raw file on disk to a fully understood set of behaviors: what its static properties looked like, what it did once executed, what its unpacked payload actually contained, and how it persisted, injected, and talked to a command-and-control server. All of that work produces something valuable, but on its own it's a one-off writeup about one sample. This chapter is where that understanding gets converted into something reusable: a YARA rule that matches the file or its variants, a Sigma rule that catches the behavior regardless of which sample produced it, and a mapping back to ATT&CK that tells a team what its detection coverage actually looks like.
From One Sample to Reusable Detection Content
Why One Analysis Isn't Enough
It helps to be honest about what a completed malware analysis actually is on its own: a detailed, accurate description of one specific file's behavior, at one specific point in time. That description is useful for the immediate incident, closing out a ticket, briefing an IR lead, deciding whether a host needs to be rebuilt, but it doesn't protect anyone from the next sample the same actor ships. Threat actors change hashes constantly. A loader gets recompiled, repacked, or has a single string altered, and every hash-based indicator built from the previous sample stops matching immediately, while the underlying tradecraft, the mutex naming convention, the injection technique, the C2 beaconing cadence, often stays identical for months.
What Detection Engineering Asks
Detection engineering is the discipline of writing that description down in a form a machine can act on automatically, at a level of abstraction durable enough to survive the small changes an actor makes between builds. That's a genuinely different skill from analysis itself. Analysis asks "what is this file doing." Detection engineering asks "what is true about this file, or about this behavior, that will still be true the next time I see something like it, and how specific does my rule need to be to catch that without also catching half the legitimate software running in the environment." Getting that balance right is most of what this chapter covers.
YARA vs Sigma: Different Layers
| YARA | Sigma | |
|---|---|---|
| Works against | Files and file content: byte patterns, strings, structural properties in the sample itself | OS telemetry generated while a process actually runs: process creation, remote thread creation, registry writes |
| Typical deployment point | A file share, an email attachment, an EDR file-write event | The behavioral signal dynamic analysis surfaced in Chapter 3 |
Neither replaces the other. A mature detection program builds both from the same piece of analysis, because a hash-evading repack defeats YARA content matching in a way it can't defeat a behavioral Sigma rule watching what the process actually does once it runs.
YARA Fundamentals: Pattern Matching Against Files
YARA describes itself, accurately, as a tool aimed at helping malware researchers identify and classify malware samples, and the mechanism behind that is pattern matching against textual or binary content. A YARA rule doesn't understand what a file does. It only checks whether the file contains a defined set of patterns arranged according to a defined boolean condition, and it does that extremely fast across large file sets, which is exactly why it sits at the front of most detection pipelines, in EDR file-scan engines, in email gateway attachment scanning, and in retro-hunting across an existing file corpus once new intelligence comes in.
The Rule Skeleton
Every YARA rule follows the same skeleton. A rule starts with the keyword rule followed by a name, which has to be unique within whatever ruleset it belongs to and can't start with a digit. Inside the rule body, an optional meta block holds free-form key-value pairs, author, description, creation date, a reference to the sample or campaign the rule was built from, none of which affect matching but all of which matter enormously once a ruleset grows past a handful of entries and someone other than the original author needs to understand why a rule exists. The strings block is where the actual patterns live, each one given a local identifier prefixed with a dollar sign. A string can be a plain text string, a hex byte sequence for matching binary content that doesn't render as readable text, or a regular expression for patterns that vary in small, structured ways. The condition block is the only required section besides the rule name, and it's a boolean expression over those identifiers: $a and $b, 2 of them to require any two of the defined strings, all of them, or arithmetic and file-size checks combined with string matches using standard boolean operators.
Hex Patterns for Binary Content
Hex patterns matter more than they might seem to at first glance, because a lot of what's structurally interesting about a piece of malware doesn't survive as a printable string. A byte sequence corresponding to a specific shellcode stub, an encoded configuration blob, or a distinctive instruction sequence at a function's entry point has to be matched as raw bytes, and YARA's hex string syntax supports wildcard nibbles (written as ??) and jump ranges (written as [2-4], meaning "skip somewhere between two and four bytes here") specifically so a pattern can tolerate the small byte-level differences that show up between a compiler's debug and release builds, or between two samples compiled a few weeks apart from a slowly evolving codebase.
The PE Module: Structural Conditions
The built-in pe module is where YARA stops being purely a strings-and-bytes tool and starts being able to reason about the PE structure Chapter 2 covered directly. A condition can reference pe.imphash() to match a known import-hash value, pe.number_of_sections or a named section's pe.sections[i].characteristics to catch structural oddities a packer introduces, or pe.imports("kernel32.dll", "VirtualAllocEx") to require a specific import regardless of where it sits in the import table. That's the direct link back to static analysis: the imphash you pulled in Chapter 2, the section anomaly that told you the sample was packed, the import combination that hinted at process injection before you'd even run the sample, all become first-class conditions a YARA rule can check without ever touching the file's raw strings.
| Rule component | Required | Purpose |
|---|---|---|
| Rule name | Yes | Unique identifier for the rule within its ruleset |
| meta | No | Author, description, date, and reference metadata; ignored during matching |
| strings | No* | Named text, hex, or regex patterns the condition can reference |
| condition | Yes | Boolean logic over the defined strings and, optionally, PE module fields |
*A rule can theoretically define a condition using only PE module fields with no strings block at all, but nearly every practical rule combines both.
pe, elf, math, and other modules with a simple import "pe" statement at the top of the rule file. The module has to be imported before its fields are usable in a condition, and forgetting the import line is one of the most common reasons a rule silently fails to compile.
Writing a YARA Rule from Real Analysis Output
Choosing Artifacts from Analysis Notes
The most reliable way to build a YARA rule is to walk backward through your own analysis notes and ask which specific artifacts were distinctive enough to be worth encoding. Take a hypothetical loader sample from earlier in this module: Chapter 2's string analysis turned up a mutex name the sample creates on startup to prevent two copies from running at once, along with a decoy error string the sample displays if a required command-line argument is missing, and Chapter 2's import table review flagged a combination of VirtualAllocEx and WriteProcessMemory, the same primitive pairing Chapter 5 covered as the mechanical core of classic process injection.
Building the Condition
Each of those becomes a named string. The mutex name and the error string are plain ASCII (the mutex might also be worth matching as a wide string, since Windows API calls frequently pass mutex names as UTF-16), and the three imports are plain ASCII function names pulled straight from the import table. The condition ties them together: check the PE magic bytes and a reasonable file-size ceiling to cheaply rule out anything that clearly isn't a match before running the more expensive string checks, require the mutex string since it's the single most distinctive artifact in the sample, and require at least two of the three injection-related imports rather than all three, since a variant that swaps WriteProcessMemory for NtWriteVirtualMemory should still match.
rule Loader_GenericStager_Mutex
{
meta:
author = "H3AD-SEC"
description = "Generic loader stager: startup mutex, decoy error string, and injection-related import pairing"
date = "2026-09-01"
strings:
$mutex = "Global\\SvcHostMtx_9F2A" wide ascii
$err = "Failed to initialize update channel" ascii
$api1 = "VirtualAllocEx"
$api2 = "WriteProcessMemory"
$api3 = "NtWriteVirtualMemory"
condition:
uint16(0) == 0x5A4D and
filesize < 2MB and
$mutex and
2 of ($api1, $api2, $api3)
}
The mutex string is required outright since it's the rarest, most distinctive artifact here. The import pairing uses "2 of" rather than "all of" so a build that renames or substitutes one API call still matches on the other two.
Precision vs Durability: The Central Tradeoff
| Narrow Rule (exact mutex, error string, all 3 imports) | Loose Rule (just VirtualAllocEx + WriteProcessMemory) | |
|---|---|---|
| False positives | Almost never fires on legitimate software | Fires on debuggers, some anti-cheat/DRM tooling, and automation frameworks that call the same two APIs |
| Durability | Stops matching the moment the actor changes the mutex name, which costs them nothing | Catches far more loader variants |
In practice, the mutex name and decoy string in this example carry almost all of the rule's precision, and the import pairing carries almost all of its durability against minor recompilation. That split, one or two highly specific artifacts for precision, one broader behavioral or structural condition for durability, is close to the pattern that shows up in most well-tuned YARA rules built from real analysis, regardless of malware family.
Sigma for Process Creation and Behavioral Telemetry
The Cloud Security module covered Sigma as a vendor-neutral YAML format for log-based detection, a logsource block, a detection block of named selections, and a condition tying them together, written once and converted into whatever native query syntax a given SIEM or EDR actually runs. That same structure applies directly to endpoint behavioral telemetry, and two Sysmon event types carry the most weight for the tradecraft covered in Chapter 5.
| Event | Fires On | Detection Value |
|---|---|---|
| Sysmon Event ID 1 (process creation) | Every new process start; carries the parent-child relationship, full command line, and image hash | Backbone of most process-based detection: a suspicious pairing like winword.exe spawning powershell.exe, or a command line with an encoded PowerShell payload |
| Sysmon Event ID 8 (CreateRemoteThread) | One process creating a thread inside another process's address space, the mechanical step behind classic and reflective process injection | Narrower but more directly diagnostic for injection; a remote thread created inside lsass.exe by anything outside a small set of legitimate callers is close to always worth attention |
title: Suspicious CreateRemoteThread Targeting LSASS
id: 6a9d2f14-3b7c-4e91-8a5d-2f7c9b1e4d60
status: experimental
description: Detects a CreateRemoteThread event where the target process is lsass.exe, consistent with process injection used to access credential material in memory.
logsource:
category: create_remote_thread
product: windows
detection:
selection:
TargetImage|endswith: '\lsass.exe'
filter_known_dumpers:
SourceImage|endswith:
- '\werfault.exe'
- '\wermgr.exe'
condition: selection and not filter_known_dumpers
level: high
tags:
- attack.credential-access
- attack.t1055
The filter excludes Windows Error Reporting processes, which legitimately create remote threads into lsass.exe while capturing a crash dump, and are one of the few genuinely common sources of benign matches against this selection. The tag maps to ATT&CK T1055, Process Injection, the technique family Chapter 5 covered mechanically.
Notice what this rule doesn't try to do: it doesn't attempt to identify which injection technique was used, classic DLL injection, process hollowing, or a reflective loader, because Event ID 8 alone doesn't carry that level of detail. What it catches is the behavior common to nearly all of them, a foreign process creating execution inside lsass.exe, which is exactly the durability tradeoff Sigma is built for. A YARA rule matching a specific injection loader's strings breaks the moment that loader gets rebuilt. A Sigma rule watching for the resulting CreateRemoteThread call against a sensitive target process keeps working against any loader that performs that same mechanical step, regardless of what it looks like on disk.
From IOC Extraction to a Detection Pipeline
Categories of Indicators and Their Lifespan
A single analysis run, following the earlier chapters' workflow, produces several different categories of indicator, and each category has a different useful lifespan. Chapter 3's dynamic analysis surfaces network indicators (C2 domains and IPs, URIs the malware requests), file-system artifacts (dropped file paths and names), and registry modifications. Chapter 5's tradecraft review surfaces the persistence mechanism, the injection technique, and the C2 beaconing pattern. None of these belong in the same bucket, because they don't survive the same amount of change on the actor's side.
Why Durability Differs by Layer
File hashes and the exact strings a YARA rule matches are the least durable category. A threat actor who recompiles their loader, changes a single configuration string, or runs it through a different packer invalidates a hash-based detection instantly and at essentially zero cost to themselves. That's not a reason to skip hash-based detection, it's cheap to generate, fast to run at scale, and genuinely useful against exact and near-exact matches, particularly for catching a known-bad file being reused unmodified across multiple intrusions. It's a reason not to rely on it as the only layer. Sigma and EDR behavioral rules built around technique-level signals, the CreateRemoteThread pattern above, a specific registry key written for persistence, a beaconing interval and jitter pattern, survive exactly the kind of minor variation that breaks hash and string matching, because they're built around what the actor has to do mechanically to achieve their goal, not around the specific bytes of any one build. Network indicators, C2 domains and IPs pulled from dynamic analysis, sit in their own category again: they're durable against sample recompilation but not against infrastructure rotation, which is why they typically feed a threat-intel blocklist or a firewall/proxy rule rather than an endpoint detection rule, and why they need a refresh cadence of their own.
| Detection layer | Scope | Durability against sample changes | Primary false-positive risk |
|---|---|---|---|
| Hash-based (SHA-256, imphash) | One exact file, or files sharing an import table | Low; broken by any recompile or repack | Very low, but coverage collapses fast |
| YARA (strings, hex, PE module) | A family or variant sharing artifacts or structure | Moderate; depends on how specific the strings are | Moderate if strings or conditions are too generic |
| Sigma / EDR behavioral rules | A technique, regardless of which sample used it | High; survives recompilation and repacking | Depends heavily on tuning against normal admin/EDR activity |
Running the Layers Together
A functioning detection pipeline runs all three layers together rather than picking one:
- Hash and YARA matching happen early, often at the file-scan or EDR-agent level, catching known-bad content before it ever executes.
- Sigma and EDR behavioral rules run continuously against process and API telemetry, catching the technique even when the specific file evades the first layer entirely.
- Network indicators feed a separate blocklist that doesn't care what file is talking to that C2 domain at all.
Losing any one layer doesn't collapse the whole pipeline, which is the entire point of building three layers instead of leaning on whichever one was easiest to stand up first.
Mapping Detections to ATT&CK
Why Tags Matter
A ruleset without ATT&CK tags is a list of things that fire alerts. A ruleset with ATT&CK tags is a map of what a team can actually see happening in its environment, which is a different and more useful object. Tagging a YARA or Sigma rule with the technique or techniques it covers, the way the Sigma example above tags T1055 for process injection, turns individual rules into data a team can aggregate: which tactics have deep coverage, which have exactly one brittle rule, and which have none at all.
Tag What the Rule Detects, Not the Family's Reputation
The tagging discipline itself is simple to describe and easy to get wrong in practice. A rule gets tagged with the technique it actually detects, not the technique the underlying malware family is best known for overall. The CreateRemoteThread-against-lsass rule earlier in this chapter is tagged T1055, Process Injection, because that's the mechanical behavior the rule's detection block matches. A separate rule watching for a new autostart registry key would get tagged against the relevant persistence technique instead, and a rule specifically watching for credential material being read out of LSASS process memory, as opposed to a thread merely being created inside it, would map to the credential-access technique for LSASS memory access rather than to injection at all. Tagging based on "this is generally ransomware behavior" instead of the specific technique a rule's logic actually matches is how ATT&CK coverage maps end up misleading, showing a tactic as covered when only one narrow variant of it actually is.
Done consistently across a ruleset, this mapping is what connects Chapter 5's tradecraft directly to something testable. Chapter 5 walked through persistence mechanisms, injection techniques, and C2 beaconing as concepts. Tagging each rule against the technique it covers turns that conceptual coverage into a concrete question a team can answer on demand: do we have a working detection for process injection into a sensitive process, yes, here's the rule and here's the last time it fired in a purple-team exercise. Do we have one for the specific persistence technique this campaign used, and if the honest answer is no, that gap is now visible and prioritizable instead of buried inside a pile of rules nobody has mapped against anything.
The Pivot Point of the Module
This chapter is where the module's center of gravity shifts. Chapters 1 through 5 were about reading a single sample accurately, its static properties, its dynamic behavior, its unpacked payload, its persistence and injection and C2 mechanics. Everything in this chapter takes that understanding and asks a different question: not "what did this sample do," but "what will still be true the next time a related sample shows up, and how do I write that down so a machine catches it automatically." A YARA rule built from a mutex name and an import pairing, a Sigma rule watching for CreateRemoteThread against lsass.exe, an ATT&CK tag connecting that rule to a named technique, are all the same kind of artifact: analysis converted into something that keeps working after the one sample that inspired it is long gone.
Chapter 7 picks this up directly. The threat actor campaigns and loader-to-ransomware ecosystems covered there aren't abstract case studies, they're examples of exactly this kind of detection content being built, tested against real intrusions, and refined as the actors behind them change their tooling. Understanding how a YARA rule gets narrowed after a false-positive report, or how a Sigma rule gets a new exclusion added after a legitimate admin tool trips it, is a lot easier once you've seen, as this chapter walked through, where that rule came from in the first place.
Key Takeaways
- YARA matches patterns in file content: a rule name, an optional meta block, a strings block of text, hex, or regex patterns, and a required condition block combining them with boolean logic. The pe module lets a condition reference PE structure directly, imphash, section characteristics, or specific imports.
- A well-built YARA rule usually splits its condition into one or two highly specific artifacts for precision (a mutex name, a decoy string) and one broader structural or import-based condition for durability against minor recompilation.
- Always test a new YARA rule against a known-clean corpus before deploying it. A rule tuned only against the malicious sample that inspired it has no evidence about how it behaves against legitimate software.
- Sigma applies the same logsource/detection/condition structure to endpoint behavioral telemetry. Sysmon Event ID 1 (process creation) and Event ID 8 (CreateRemoteThread) are the two event types most directly tied to injection tradecraft, and a CreateRemoteThread event targeting lsass.exe is one of the more diagnostic signals available.
- A real detection pipeline layers hash-based matching, YARA, and Sigma/EDR behavioral rules together, since each layer survives a different kind of change on the actor's side; relying on file hashes alone collapses the moment a sample is recompiled or repacked.
- Tagging each rule with the specific ATT&CK technique its logic actually matches, not the technique the malware family is broadly known for, turns a list of rules into an honest map of technique-level coverage.
Knowledge Check
Click an answer to reveal the explanation.
In the YARA loader example in this chapter, why does the condition use "2 of ($api1, $api2, $api3)" for the injection-related imports instead of requiring all three?
Why does a Sigma rule watching for a CreateRemoteThread event targeting lsass.exe tend to be more durable than a YARA rule built from one loader sample's strings?
What is the correct way to tag a detection rule with an ATT&CK technique?