DEV Community

David Moya
David Moya

Posted on

YARA Rules for Beginners: Writing Your First Malware Detection Signature

YARA Rules for Beginners: Writing Your First Malware Detection Signature

TL;DR: YARA is a pattern-matching engine used by malware analysts, incident responders, and SOC teams to identify malicious files based on textual or binary signatures. In this tutorial, you'll learn YARA's syntax, write your first rule against a real-world sample (the EICAR test file and a WannaCry-style indicator), test it with the yara CLI, and avoid common pitfalls like false positives and performance bottlenecks. By the end, you'll have a working detection signature and a mental model for iterating on it.


What Is YARA and Why Should You Care?

YARA is a free, open-source tool originally developed by Victor Alvarez at VirusTotal. It lets you describe malware families in a readable, rule-based syntax and then scan files, memory, or processes to see if they match. Think of it as grep for binary data — but with boolean logic, wildcards, regex, and a scoring system.

It's used everywhere:

  • VirusTotal uses YARA to tag samples.
  • MalwareBazaar and MISP distribute YARA rules alongside IOCs.
  • EDR and AV vendors embed YARA-style logic (or an equivalent) in their engines.
  • Incident responders drop YARA into Velociraptor, Loki, or THOR to sweep endpoints for known threats.

If you work in threat intel or DFIR, YARA is a baseline skill. For a broader overview of how YARA fits alongside Sigma and Suricata in a detection stack, this YARA rules guide covers the detection-triangle concept well.

Let's write a rule.


Anatomy of a YARA Rule

Every YARA rule has three parts (plus optional metadata):

rule RuleName
{
    meta:
        author = "you"
        description = "what it detects"
        date = "2024-01-01"

    strings:
        $a = "malicious string"
        $b = { 4D 5A 90 00 }        // hex
        $c = /regex[0-9]{3}/i        // regex

    condition:
        $a and $b
}
Enter fullscreen mode Exit fullscreen mode

Key rules:

  1. Rule names must start with a letter or underscore and contain only alphanumerics and underscores.
  2. Strings start with $. You can have up to 10,000 of them per rule (but don't).
  3. Condition is a boolean expression returning true or false.
  4. Modifiers like nocase, wide, ascii, fullword change how strings match.

String types at a glance

Type Example Notes
Text $a = "cmd.exe" Case-sensitive by default
Text + nocase $a = "cmd.exe" nocase Case-insensitive
Wide $a = "cmd.exe" wide UTF-16LE — common in Windows binaries
Hex $a = { 4D 5A } Raw bytes, supports wildcards ??
Regex $a = /https?:\/\/[a-z0-9.]+/i Slower than plain strings

Condition operators

  • Logical: and, or, not
  • Comparison: ==, !=, <, >, <=, >=
  • Counting: #a > 5 (string $a appears more than 5 times)
  • Size: filesize < 1MB
  • Location: $a at 0 (string is at offset 0)
  • Sets: any of them, all of ($a*), 2 of ($b*)

Writing Your First Rule: The EICAR Test File

EICAR is a harmless 68-byte string that AV engines intentionally flag as malware. It's perfect for a first rule because the signature is unambiguous.

The EICAR string is:

X5O!P%@AP[4\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H*
Enter fullscreen mode Exit fullscreen mode

Create eicar.yar:

rule EICAR_Test_File
{
    meta:
        author = "dev.to reader"
        description = "Detects the EICAR antivirus test file"
        reference = "https://www.eicar.org/download-anti-malware-testfile/"
        date = "2024-11-01"

    strings:
        $eicar = "X5O!P%@AP[4\\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H*"

    condition:
        $eicar
}
Enter fullscreen mode Exit fullscreen mode

Note the escaped backslashes: because YARA strings interpret \ as an escape character, a literal backslash must be \\.

Save the EICAR payload:

printf 'X5O!P%%@AP[4\\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H*' > sample.txt
Enter fullscreen mode Exit fullscreen mode

Install YARA (Ubuntu/Debian):

sudo apt install yara
Enter fullscreen mode Exit fullscreen mode

Run it:

yara eicar.yar sample.txt
Enter fullscreen mode Exit fullscreen mode

Output:

EICAR_Test_File sample.txt
Enter fullscreen mode Exit fullscreen mode

You just wrote and executed a detection rule. If it doesn't match, verify the file contents with xxd sample.txt — off-by-one characters are the usual culprit.


A More Realistic Example: Detecting WannaCry-Style Indicators

The EICAR rule is trivial. Real malware families require layered logic — anchoring on file structure, unique strings, and behavioral telltales.

WannaCry (2017) spread via EternalBlue (MS17-010), encrypted files with AES-128, and demanded Bitcoin payment. Its binary contains several distinctive artifacts. Even though AV coverage is universal now, it's a great teaching sample because its indicators are well-documented.

Here's a simplified rule inspired by public WannaCry detections:

rule WannaCry_Ransomware_Indicators
{
    meta:
        author = "dev.to reader"
        description = "Detects WannaCry-style dropper artifacts"
        reference = "https://www.cisa.gov/news-events/alerts/2017/05/12/wannacrypt-ransomware"
        date = "2024-11-01"
        tlp = "white"

    strings:
        // PE header
        $mz = { 4D 5A }

        // WannaCry kill switch domains (historical)
        $killswitch = "iuqerfsodp9ifjaposdfjhgosurijfaewrwergwea.com" nocase
        $killswitch2 = "ifferfsodp9ifjaposdfjhgosurijfaewrwergwea.com" nocase

        // Ransom note filename
        $note = "@Please_Read_Me@.txt" nocase

        // Bitcoin wallet strings used in the original campaign
        $btc1 = "13AM4VW2dhxYgXeQepoHkHSQuy6NgaEb94"
        $btc2 = "12t9YDPgwueZ9NyMgw519p7AA8isjr6SMw"

        // Tor client artifacts
        $tor = "taskdl.exe" nocase
        $tor2 = "taskse.exe" nocase

        // Encryption-related imports
        $crypto = "CryptEncrypt" ascii
        $crypto_w = "CryptEncrypt" wide

    condition:
        $mz at 0 and
        (2 of ($killswitch*) or
         $note or
         any of ($btc*) or
         (2 of ($tor*) and 1 of ($crypto*)))
}
Enter fullscreen mode Exit fullscreen mode

Let's unpack the design choices:

  1. $mz at 0 anchors the file as a PE. Without it, a text file quoting the BTC address would match — a textbook false positive.
  2. nocase on domains catches case variations that survive string obfuscation.
  3. any of ($btc*) uses wildcarded set matching, so adding more wallets later is cheap.
  4. The condition is an OR of independent clusters. Malware mutates; requiring all indicators would miss variants. Requiring some combination balances recall and precision.
  5. $crypto and $crypto_w handle both ASCII and UTF-16LE import tables; Windows API strings often appear in either form.

This rule is intentionally illustrative — production rules would also factor in entropy, PE section anomalies, and signed-certificate checks. For a broader writeup on building detection logic from threat intel feeds, the team at Riskitera's blog publishes hands-on analyses worth following if you want to see how rule authors source their indicators.

Test it against any WannaCry sample in a safe sandbox (never on a production host):

yara -r wannacry.yar /path/to/samples/
Enter fullscreen mode Exit fullscreen mode

Flags you'll use often:

  • -r — recurse directories
  • -s — print matching strings (essential for tuning)
  • -w — disable warnings
  • -p N — parallel scan with N threads
  • -d name=value — define external variables

Example with string output:

yara -s wannacry.yar suspicious.exe
Enter fullscreen mode Exit fullscreen mode

You'll see exactly which strings fired, which tells you whether the match is real or a coincidental string in an unrelated binary.


Best Practices for YARA Rule Quality

Writing a rule that matches once is easy. Writing one that survives in production for years is hard. These habits matter:

1. Anchor on file structure

Always include uint16(0) == 0x5A4D (MZ header) when targeting Windows executables. For scripts, anchor on shebangs. For documents, anchor on magic bytes.

condition:
    uint16(0) == 0x5A4D and
    filesize < 5MB and
    $unique_string
Enter fullscreen mode Exit fullscreen mode

2. Prefer multiple short strings over one long one

Long strings break on trivial mutations. Four independent 8–15 byte strings joined by 3 of them are more robust.

3. Use fullword and nocase deliberately

fullword prevents cmd.exe from matching inside xcmd.exe. nocase triples performance cost in some engines — use when case obfuscation is realistic (domain names, CLI arguments), not for every string.

4. Beware of common strings

Strings like "This program cannot be run in DOS mode" and "kernel32.dll" appear in millions of benign files. Never condition on them alone. Combine with a filesize bound or a rare companion string.

5. Add metadata and TLP

Future-you will thank present-you. Include author, description, date, reference, hash (of the reference sample), and tlp. Most sharing platforms (MISP, OpenCTI, ThreatFox) parse these fields.

6. Test for false positives before publishing

Run your rule against a clean corpus — /usr/bin on Linux, C:\Windows\System32 on Windows — and confirm zero hits. A quick smoke test:

yara -r myrule.yar /usr/bin 2>/dev/null | head
Enter fullscreen mode Exit fullscreen mode

If anything matches, inspect it with -s and tighten the condition.

7. Version your rules

Track changes in Git. Use semantic versioning on rule sets. A rule you tweak to catch variant B might silently stop catching variant A.

8. Use include and modules

YARA supports modules like pe, math, hash, and elf that expose parsed file metadata:

import "pe"
import "hash"

rule Suspicious_Packed_PE
{
    condition:
        pe.is_pe and
        pe.number_of_sections > 0 and
        for any section in pe.sections : (
            section.characteristics & pe.SECTION_MEM_EXECUTE and
            math.entropy(section.offset, section.size) > 7.0
        ) and
        hash.md5(0, filesize) != "d41d8cd98f00b204e9800998ecf8427e"
}
Enter fullscreen mode Exit fullscreen mode

That's a real, generic heuristic: packed executables often exhibit high entropy in executable sections, which is a common sign of runtime unpacking.


Common Pitfalls and How to Avoid Them

Forgetting to escape backslashes. "C:\Windows" in YARA is a literal backslash followed by W. You want "C:\\Windows". Regex strings use different escaping.

Using $a without context in condition. YARA treats $a as "string defined AND found." If you write not $a, it returns true when $a is undefined — probably not what you meant. Use explicit #a == 0 for clarity.

Overusing regex. Regex is slow and error-prone. Prefer hex wildcards { 4D 5A ?? ?? 00 } for binary signatures and nocase strings for text.

Ignoring filesize. Limiting to filesize < 10MB often eliminates a huge class of false positives (archives, media files) without losing real matches.

Scanning the filesystem blindly. On production endpoints, scan only file types you can act on (executables, scripts, office docs) and exclude caches. Use -N to skip .git, node_modules, and the like.

Copying rules without attribution. Public rules from vendors and researchers carry licenses and TLP markings. Respect them.


Conclusion

YARA is a deceptively simple language that scales from one-line text matches to complex PE-aware heuristics. The path to competence is:

  1. Write a trivial rule (EICAR) to learn syntax.
  2. Model a real family (WannaCry) to learn layered conditions.
  3. Test against clean corpora to build intuition for false positives.
  4. Iterate with -s output to see why something matched.
  5. Version and document so your rules survive the next analyst.

The tooling is straightforward: apt install yara, drop a .yar file, run yara rule.yar target. The craft is in the condition blocks. Start small, anchor on structure, and treat every rule as a hypothesis you have to falsify against a clean system before trusting it on an infected one.

Once you're comfortable, look into YARA-X (the Rust rewrite from VirusTotal), integrate rules into your SIEM or EDR, and pair them with behavioral detections. Static signatures catch known families; behavioral logic catches their mutations. You want both.

Top comments (0)