H3AD-REF / GUIDES / YARA RULE WRITING

YARA Rules.
Written Right, Not Just Written.

A static reference for YARA syntax and judgment calls: what each section of a rule does, how string and condition types differ, and the habits that separate a rule that hunts from one that just makes noise. No rule engine here, no live testing, just the reference. A written rule can be run against a live memory image with the windows.vadyarascan plugin covered in Volatility3 Commands.

Rule Anatomy

Four sections, one of them required. Get the shape right and the rest of YARA is just vocabulary.

The Four Sections, In Order

1. import (optional)
import "pe";  or  import "math";
├── Placed at the top of the file, outside any rule body
└── Unlocks module functions like pe.imports() and math.entropy() for use in condition:
    // only needed if condition: actually calls a module function

2. meta:
key = "value" pairs, free text
├── Never evaluated, documentation only, not matching logic
├── author, date, description are conventional
└── reference should map to a sample hash, CVE, or ATT&CK ID
    // a rule with no reference is unmaintainable once someone else has to triage its hits

3. strings:
$name = pattern, one per line
├── Text:  $s1 = "powershell" nocase
├── Hex:   $hex1 = { 46 72 6F 6D 42 61 73 65 36 34 }
└── Regex: $r1 = /-enc(odedcommand)?/i
    // referenced by name ($s1, $hex1, ...) from inside condition:

4. condition: required
The boolean expression that decides whether the rule fires
├── Can reference strings ($s1), counts (#s1), offsets (@s1), file properties, module functions
└── The only section YARA actually requires
    // a rule with no strings: can still match on file checks alone, e.g. filesize and uint16(0)

String Types

A string's type decides how it searches and what it costs at scan time. Pick lazy and pay for it on every file.

STRINGS

Text Strings

Modifiers change how a plain string matches, not just what it matches
  • nocase — case-insensitive match
  • wide — match UTF-16LE, needed for Windows API strings and PowerShell's in-memory text
  • ascii — match single-byte ASCII (the default; pair with wide to catch both encodings of the same string)
  • fullword — match can't be flanked by another word character, cuts false positives on short strings
  • xor — matches the string XOR'd against a single byte, useful for lightly-obfuscated payloads
  • $s2 = "cmd.exe" wide ascii fullword — catches both text encodings as a whole word, not a substring
STRINGS

Hex Strings

Byte-exact matching with wildcards for the parts that vary
  • ?? — wildcard byte, matches anything at that position
  • [0-4] — jump range, allows 0 to 4 unknown bytes between two fixed byte groups
  • { 46 72 6F 6D 42 61 73 65 36 34 } — matches the ASCII bytes for "FromBase64"
  • { E8 ?? ?? ?? ?? 5D C3 } — a CALL instruction with an unknown 4-byte offset, followed by a fixed epilogue
STRINGS

Regex Strings

The most expressive string type, and the slowest one YARA evaluates
Use $r = /pattern/ only when a text or hex string genuinely can't express what you're matching. An unanchored pattern with nested quantifiers can cause catastrophic backtracking on a large file, since a regex string is checked at every byte offset, not once. Anchor with ^/$ or bound the length where you can, before shipping a regex string into a rule that scans a whole file share.

Condition Operators

condition: is the only line that decides a match. Everything else in the rule just gives it vocabulary to work with.

CONDITION

Boolean Logic

The connective tissue between every other check in the rule
  • and / or / not — combine string matches and other conditions
  • $s1 and ($s2 or $s3) and not $s4 — parentheses group sub-conditions exactly like any other boolean expression
CONDITION

Counting

Match on how many strings hit, not just whether they did
  • 2 of ($s1,$s2,$s3) — at least 2 of the listed strings matched
  • any of them / all of them — shorthand over every $string defined in the rule
  • #s1 > 3 — string $s1 matched more than 3 times in the file
CONDITION

Positional

Where a match happens can matter as much as whether it happens
  • $a at 0 — string $a matches at offset 0, the start of the file
  • $a in (0..1024) — string $a matches somewhere in the first 1024 bytes
CONDITION

File Checks

Test the file's shape before you ever look at its content
  • filesize > 100KB — reject anything too small to be a real sample of what you're hunting
  • uint16(0) == 0x5A4D — first two bytes read as little-endian uint16 equal MZ, the PE header magic
CONDITION

Module Functions

Requires an import at the top of the file; unlocks checks plain strings can't do
  • pe.imports("kernel32.dll", "CreateProcessW") — flags a specific imported API by DLL and function name
  • math.entropy(0, filesize) > 7.0 — flags high-entropy content typical of packed or encrypted data

Best Practices

The difference between a rule that hunts and a rule that pages someone at 3am for nothing.

PRACTICE

Fullword + Nocase

Two modifiers, most of your false-positive reduction
Add fullword and nocase to plaintext strings by default. Fullword stops a short string from matching as a substring of something longer; nocase stops case variations from slipping past a rule that only checked one spelling.
PRACTICE

Anchor On Structure

A string match alone is an opinion, not evidence
Pair string matches with filesize, uint16(0), or a pe.* check so the rule tests the file's shape, not only its content. Two rules that share the same strings but differ on structure checks will have very different false-positive rates in production.
PRACTICE

Use Private Rules

Reusable sub-conditions that never fire on their own
Declare a private rule for a shared building block, like "looks like a PE" or "has a suspicious PowerShell flag," and reference it from other rules as a condition term. It never appears in scan output by itself, only through whatever rule calls it.
PRACTICE

One Rule, One Hypothesis

Don't merge unrelated families to save a rule slot
OR-ing together strings from two different malware families into one rule saves a line of YARA and costs an analyst a triage step every time it fires, because the hit no longer tells them which family they're looking at.
PRACTICE

Test Against Clean Files

The sample that wrote the rule is not the sample that will break it
Run every new rule against a corpus of known-clean files before it ships, not just the malicious sample it was written from. A rule that has only ever seen the one sample it targets will false-positive the first time it meets ordinary software.
PRACTICE

Pair Entropy With Strings

High entropy alone describes most compressed files on a system
math.entropy(0, filesize) > 7.0 flags packed or encrypted content, but on its own it also flags routine zip archives, images, and installers. Pair it with a small, specific string set so the entropy check narrows a real hit instead of carrying the rule by itself.

Common Pitfalls

Small mistakes that don't show up until the rule is already deployed, and already wrong.

PITFALL

Forgetting Wide

The string is there. Your rule just can't see it.
Windows API strings and PowerShell's in-memory text are UTF-16LE, not ASCII. A plain $s = "powershell" string will silently never match either one; add wide, or wide ascii to catch both encodings.
PITFALL

Unanchored Regex

One bad pattern can pin a CPU core for the whole scan
A pattern like /.*a.*b.*c/ against a large file can trigger catastrophic backtracking well before it ever reaches a verdict. Anchor with ^/$, or switch to a text or hex string if the pattern doesn't actually need regex.
PITFALL

Missing meta.reference

A rule with no source becomes unmaintainable the moment someone else inherits it
Every rule should link back to what it was written from: a sample hash, an ATT&CK ID, a report URL. Without meta.reference, the next analyst triaging a hit has to reverse-engineer why the rule exists before deciding whether it still should.

Full Example

Every clause from the sections above, in one rule, doing real work.

Suspicious_PowerShell_EncodedCommand

rule Suspicious_PowerShell_EncodedCommand
{
    meta:
        author = "your_name"
        date = "2026-09-20"
        description = "Detects base64-encoded PowerShell execution"
        reference = "T1059.001"

    strings:
        $s1 = "powershell" nocase
        $s2 = "-EncodedCommand" nocase
        $s3 = "-enc" nocase
        $hex1 = { 46 72 6F 6D 42 61 73 65 36 34 }

    condition:
        $s1 and ($s2 or $s3) and $hex1
}

// $hex1 decodes to the ASCII string "FromBase64" — pairing it with the plaintext flags means the
// rule needs both the command-line indicator and the decode-side evidence before it fires.
// meta.reference maps the hit straight to ATT&CK T1059.001 for triage.