Skip to content
malwareintermediate#yara#malware-analysis#detection-engineering#threat-intel#reverse-engineering

YARA Rules: Pattern Matching for Malware Identification

How YARA rules work: strings, hex patterns with wildcards, condition syntax, the PE and math modules, imphash and entropy checks, plus the mistakes that make a rule slow or useless.

A file hash identifies one file. Change one byte and the hash is useless, which is why a signature list of hashes is always behind and why the same malware family produces thousands of them.

YARA describes what a family looks like instead. A rule names some patterns, states a condition over them, and matches any file or memory region satisfying it. One good rule covers every build of a family, including the ones that have not been compiled yet.

This guide covers the syntax, the modules that make rules precise, the performance rules that keep a corpus fast, and the difference between a rule that survives the next build and a rule that expires with the campaign.

Scope: Intro to Malware Analysis owns the analysis workflow that produces the patterns, Malware Obfuscation Techniques owns what the author does to defeat them, and Sigma Rules Explained owns the log-based equivalent. This page owns the rule format.

The shape of a rule

import "pe"
import "math"
 
rule Example_Loader_Config
{
    meta:
        author      = "Detection Team"
        date        = "2026-08-25"
        description = "Loader family with an XOR-decoded config blob"
        reference   = "internal-case-4471"
        hash        = "<sha256 of the reference sample>"
 
    strings:
        $mz     = { 4D 5A }
        $decode = { 8A 04 0A 34 ?? 88 04 0A 42 3B D1 7C ?? }
        $cfg    = "cfg::" ascii wide
        $api1   = "VirtualAllocEx" ascii
        $api2   = "WriteProcessMemory" ascii
 
    condition:
        uint16(0) == 0x5A4D
        and filesize < 2MB
        and $decode
        and 1 of ($api*)
        and math.entropy(0, filesize) < 7.2
}

Three sections. meta is documentation and carries no matching logic, though tooling reads it, so an author, a date and a reference belong there. strings declares the patterns, each with an identifier beginning $. condition is a boolean expression, and it is the only mandatory section: a rule can have a condition and no strings.

Strings

There are three kinds, and mixing them appropriately is most of rule writing.

Text strings

    $a = "GetProcAddress" ascii
    $b = "http://" ascii wide nocase
    $c = "Sample" fullword
    $d = "config" xor(0x01-0xff)
    $e = "password" base64

The modifiers do specific jobs:

  • wide matches the string with a null byte between each character, which is how UTF-16 appears in a binary. Windows APIs are UTF-16 internally, so many strings visible as ASCII in an analysis tool are stored wide in the file. A rule that omits wide misses them. Specifying ascii wide covers both.
  • nocase is case-insensitive and slower, so use it where case genuinely varies.
  • fullword requires non-alphanumeric boundaries, which stops Sample matching inside Samples or ResampleData.
  • xor generates the string XOR-encoded with each key in the range, which finds simple obfuscation without decoding anything. It expands the pattern set considerably, so bound the range where you can.
  • base64 and base64wide generate the encodings of the string at each of the three possible alignments, since a base64 substring inside a larger blob may not start on a boundary.
  • private suppresses the string from the match output, useful when the matched data is sensitive or noisy.

Hex strings

    $code = { 8A 04 0A 34 ?? 88 04 0A 42 3B D1 7C ?? }
    $jump = { 6A 40 68 [4-8] FF 15 ?? ?? ?? ?? }
    $alt  = { E8 ( 00 | 01 ) ?? ?? ?? }

?? is a wildcard byte, and a nibble can be wildcarded too (4?). [4-8] is a jump of between four and eight arbitrary bytes, which is how a pattern tolerates a variable-length operand or a shifted address. Parentheses with | give alternatives.

Hex strings are how a rule keys on code rather than on text. Wildcarding the operands of instructions, while keeping the opcodes, produces a pattern that survives recompilation with different addresses, which is exactly the durability a family rule needs.

Regular expressions

    $re = /https?:\/\/[a-z0-9.-]{4,64}\/[a-z]{6,10}\.php/ nocase

Powerful and the slowest option. Use them where the structure genuinely varies and pin them with a literal prefix wherever possible, because YARA extracts short literal fragments (atoms) from every pattern to pre-filter with, and a regular expression that offers no usable literal forces the engine to work much harder.

Conditions

The condition is where a list of patterns becomes a detection.

    condition:
        uint16(0) == 0x5A4D          // PE magic, "MZ"
        and filesize > 10KB and filesize < 3MB
        and 2 of ($api*)             // at least two of the API strings
        and #hits > 5                // the string $hits appears more than five times
        and $marker at 0x400         // at a specific offset
        and $tag in (0..1024)        // within a range
        and all of them

The useful operators:

  • any of them, all of them, N of ($prefix*) for counting matched patterns.
  • #a is the number of occurrences of $a; @a[1] is the offset of the first occurrence; !a[1] is its length.
  • at pins a pattern to an offset, in (x..y) to a range.
  • uint8, uint16, uint32 and their big-endian variants read integers at an offset. uint16(0) == 0x5A4D is the MZ test, written little-endian.
  • filesize with KB and MB suffixes.
  • for any i in (0..pe.number_of_sections - 1) : ( ... ) iterates over structures.

Rules can also be referenced by other rules, which is how a base rule for a packer or a file type gets reused:

private rule IsPE { condition: uint16(0) == 0x5A4D }
rule Family_A { condition: IsPE and $x }

A private rule matches without reporting, and a global rule that fails short-circuits every other rule in the file, which is a convenient place to put a file-type or size gate for a whole corpus.

Modules

Modules add structural understanding that raw pattern matching cannot express.

pe parses the PE header. The high-value fields:

    pe.imphash() == "b0f2b3f2a1a68a9c2b0e56c9b5f4a1d2"
    pe.number_of_sections < 4
    pe.sections[0].name == ".text"
    pe.characteristics & pe.DLL
    pe.timestamp > 1704067200
    for any sec in pe.sections : ( math.entropy(sec.raw_data_offset, sec.raw_data_size) > 7.5 )

pe.imphash() deserves its reputation and its caveats. It is an MD5 over the normalised imported library and function names in the order the linker emitted them, so samples built from the same source with the same imports collide deliberately. That makes it excellent for grouping a family and poor as sole evidence: a packed sample has the packer's imports rather than the payload's, and two unrelated programs built with the same framework can share one.

math provides entropy and statistical measures. math.entropy(0, filesize) > 7.0 indicates compressed or encrypted content, which is the standard packed-binary heuristic. Entropy per section is more informative than entropy over the whole file, because a normal binary with one high-entropy section is the common shape of a packed payload inside an otherwise ordinary program.

hash computes hashes over a range, hash.sha256(0, filesize), which is useful for pinning one known artifact inside a broader rule.

elf, dotnet, macho and magic cover other formats. dotnet matters more than it used to, given how much commodity malware ships as .NET assemblies.

Performance

A rule corpus runs against every file on a scan. Order and pattern choice decide whether that takes minutes or hours.

Anchor cheap conditions first. YARA short-circuits, so uint16(0) == 0x5A4D and filesize < 2MB and $expensive_regex skips the regular expression entirely for every file that is not a small PE. Reversing that order runs the regular expression on everything.

Avoid short and generic patterns. A two-byte hex string or a common word forces the engine to check huge numbers of candidate positions. YARA-X and recent YARA versions warn about patterns that produce weak atoms; take the warnings seriously.

Avoid unbounded jumps. [0-] and very wide ranges defeat the pre-filter.

Prefer one specific pattern to five vague ones. Rules that require 4 of them across five weak strings tend to be both slower and less precise than one rule keyed on the decryption loop.

Test at scale before deploying. A rule that looks precise against ten samples can match a common installer stub. Run it over a clean corpus, and over a sample of ordinary files from your own estate, before it goes anywhere near an endpoint agent.

Writing a rule that lasts

The durability of a rule depends entirely on what you keyed it to.

Short-lived indicators. Campaign URLs, mutex names, hardcoded IP addresses, build paths, embedded certificates, version strings. They identify a build. They are worth including as extra evidence and worth nothing as the sole condition.

Durable indicators. A custom decryption or hashing routine as a wildcarded hex pattern. The structure of an embedded configuration blob. An unusual sequence of API resolutions. A specific combination of imports. A packer's stub. These persist across builds because changing them means changing the code, which is what separates a family rule from a hash list.

The practical method: analyse two or three samples of a family, diff them, and key the rule on what stayed the same. Anything that differs between two builds of the same family will differ in the next one too.

Running YARA

# Scan a file or directory recursively
yara -r rules/ /path/to/samples
 
# Scan a running process by PID
yara rules/loader.yar 4821
 
# Show matched strings and their offsets
yara -s rules/loader.yar sample.bin
 
# Warnings about slow or weak patterns are printed by default;
# -w suppresses them, so leave it off while developing rules
yara rules/ /samples
 
# Tag-based selection, and per-rule timeout
yara -t loader -a 60 rules/ /samples

Memory scanning is the mode that catches what disk scanning misses. A packed sample on disk exposes only the packer, and the unpacked payload exists only in memory, so a rule keyed on payload code matches the process and not the file. Rules intended for memory should avoid file-structure conditions, since a mapped region does not start with the file's header in the way uint16(0) assumes.

Beyond the command line, YARA is embedded widely: sandboxes trigger rules on submitted files, VirusTotal supports live and retrospective hunting with them, memory forensics frameworks scan process space with them, and many EDR products accept custom rules.

The verdict

YARA is a pattern language with a boolean condition on top, and its whole value is deciding what to point it at. Rules keyed on code structure, packer quirks and import combinations survive the next build. Rules keyed on strings from one campaign expire with the campaign.

The two disciplines that keep a corpus useful are anchoring conditions cheaply so scans stay fast, and validating against clean data so a rule does not fire on a legitimate installer across twenty thousand endpoints. New work should target YARA-X, which is the maintained engine, with a compatibility pass over any existing rules before switching a production scanner.

Sources & further reading