YARA is the pattern-matching language of malware analysis. Writing rules is easy; writing rules that survive the next sample variant is hard. This guide focuses on the second.

The Rule Structure

rule Example_Detection {
  meta:
    description = "Detects Example loader v3"
    author      = "CyberSecurity Elite"
    date        = "2026-04-14"
    hash        = "f3b1...c7e9"
    reference   = "https://example.com/report"
    tlp         = "white"

  strings:
    $s1 = "C2_BEACON_HEADER" ascii wide
    $s2 = { 48 8b ?? ?? 48 89 ?? E8 [4] 85 C0 }
    $s3 = /api\/v[0-9]\/(checkin|exec)/ ascii nocase

  condition:
    uint16(0) == 0x5A4D and 2 of them
}

Three string types — text, hex with wildcards/jumps, and regex. The condition is what makes the rule precise.

Anchoring: Why Most Rules Break

The first rule beginners write looks like:

strings:
  $s = "Mozilla/5.0 (compatible; MyBotV1.0)"
condition:
  $s

This catches the exact variant analyzed. The actor changes the UA string in v2 and your rule misses everything.

Better anchors:

  1. Code constructs that don’t change easily — function prologues, decryption loops, RC4 KSA initialization.
  2. Format features — PE imports, section names, RICH header values.
  3. Structural artifacts — XOR keys, callgraph fingerprints, mutex naming patterns.
rule RC4_KSA_Loop {
  meta:
    description = "Generic RC4 key-scheduling pattern"
  strings:
    // for (i=0;i<256;i++) S[i]=i; — classic init loop
    $ksa = { 31 ?? 89 ?? 24 ?? FE ?? 80 FB FF 75 ?? }
  condition:
    $ksa
}

Hex with Wildcards

$pattern = { 48 8B ?? 48 89 ?? E8 [4-6] 85 C0 74 }
//                          ^^^^^^^ jump 4-6 bytes
//             ^^ wildcard one nibble
//          ^^ wildcard one byte

Use [X-Y] for variable-length gaps, ?? for any byte, single-nibble wildcards (?2) for specific instruction encodings.

The PE Module — Best Friend for Windows Malware

import "pe"

rule Suspicious_PE_Imports {
  condition:
    pe.imports("kernel32.dll", "VirtualAllocEx") and
    pe.imports("kernel32.dll", "WriteProcessMemory") and
    pe.imports("kernel32.dll", "CreateRemoteThread") and
    pe.number_of_sections < 4 and
    pe.entry_point_section.name != ".text"
}

PE-module conditions survive simple repackers because they target the structure, not strings.

Also useful:

  • pe.rich_signature.toolid(?, ?) — compiler fingerprint, often constant across an actor’s samples
  • pe.section_index(".text").entropy > 7.0 — strong indicator of packing
  • pe.timestamp — reveals build-time patterns

Regex — Use Sparingly

YARA regex is slow. Use it only when you need true alternation or character classes that can’t be expressed in hex.

$url = /https?:\/\/[a-z0-9]{10,16}\.(top|xyz|club)\/[a-f0-9]{32}/ nocase

Performance

YARA scans terabytes at a time in SOCs. Slow rules are unusable rules.

  • Short strings (<4 bytes) ASCII-only force a heavy scan. YARA warns; listen.
  • Regex compiles to NFAs and is the slowest matcher. Combine with another condition (pe.is_pe and $regex) so YARA short-circuits.
  • for and of loops with large variable sets explode complexity.

yara --print-rules-stats profiles your ruleset.

False Positive Discipline

A noisy rule gets disabled. To stay precise:

  1. Test on goodware — Microsoft binaries, Python wheels, npm packages.
  2. Use VirusTotal Retrohunt before you publish — see what your rule actually matches across the global corpus.
  3. condition: not pe.is_dll when a string is harmless in libraries.
  4. Combine with imphash / sample-set conditions for actor-specific rules.

Rule Hygiene

Every rule should have:

  • A meaningful name (APT28_XAgent_Beacon_2024_03, not rule123)
  • meta with author, date, reference, hash
  • A clear scope in the description — what variants it covers and what it intentionally won’t
  • An explicit MIT or CC0 license header in shared repos

References