courses

YARA and YARA-X: Pattern Matching for Malware Analysis

Overview

YARA is a rule-based pattern matching engine designed for identifying and classifying malware samples. A YARA rule describes a file (or memory region) by combining string patterns, byte sequences, and Boolean logic. When a file matches all conditions in a rule, YARA reports the match — along with whichever matched strings you choose to print.

YARA is used at every stage of malware analysis:

YARA-X is a complete Rust rewrite of YARA, released as stable (v1.0.0) in June 2025. It is now the primary development target; YARA 4.x is in maintenance mode (security fixes only). VirusTotal migrated its Livehunt and Retrohunt infrastructure to YARA-X in December 2024, and saw scan timeouts drop from ~2% of files to under 0.2%.

This page covers both engines side-by-side where they differ.


Installation

YARA (Classic)

Current stable: v4.5.5 (October 2024).

# Kali / Debian
sudo apt update && sudo apt install yara

# Python bindings
pip install yara-python

# Build from source (for --with-crypto and --enable-magic modules)
sudo apt install automake libtool make gcc pkg-config \
                 libssl-dev libjansson-dev libmagic-dev
wget https://github.com/VirusTotal/yara/releases/download/v4.5.5/yara-4.5.5.tar.gz
tar xzf yara-4.5.5.tar.gz && cd yara-4.5.5
./bootstrap.sh
./configure --with-crypto --enable-magic
make && sudo make install

YARA-X

Current stable: v1.0.0+ (June 2025, with ongoing releases).

# Option 1: Pre-built binary (Linux/macOS/Windows — no Rust needed)
# Download from https://github.com/VirusTotal/yara-x/releases
# Extract and place 'yr' in PATH

# Option 2: Build from source (requires Rust)
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
git clone https://github.com/VirusTotal/yara-x
cd yara-x
cargo install --path cli

# Option 3: macOS (Homebrew)
brew install yara-x

# Python bindings
pip install yara-x

No apt package for Kali/Debian as of mid-2026. Use the pre-built release binary.


Rule Structure

A YARA rule has three optional sections and one required section:

rule RuleName : tag1 tag2 {
    meta:
        author      = "Analyst Name"
        description = "Detects Foo malware dropper"
        date        = "2025-06-01"
        reference   = "https://blog.example.com/analysis"
        hash        = "abc123def456..."
        severity    = "high"
        mitre_attack = "T1059.001"

    strings:
        $magic = { 4D 5A }
        $url   = /https?:\/\/[a-z0-9]{8,}\.(ru|cn|top)/i
        $cmd   = "cmd.exe /c" nocase

    condition:
        $magic at 0 and
        filesize < 2MB and
        any of ($url, $cmd)
}
Section Required Purpose
meta: No Key=value descriptors. String, int, or bool values. Not usable in conditions.
strings: No Named patterns ($identifier). Three types: hex, text, regex.
condition: Yes Boolean expression that determines a match.

Rule names are alphanumeric + underscore, max 128 characters. Tags follow the colon after the name; space-separated. Use -t <tag> on the command line to filter by tag.

Naming Convention

A commonly used convention for threat intelligence sharing:

THREATACTOR_MALWARE_ROLE_PLATFORM

Examples: APT28_XAgent_Loader_Windows, Lazarus_Dropper_Linux_x64, Generic_Ransomware_Encryptor.


String Types

Hex Strings

Raw byte sequences in curly braces. Support wildcards, ranges, negation, and alternatives.

strings:
    $exact   = { E2 34 A1 C8 23 FB }
    $wild    = { E2 34 ?? C8 }           // ?? = any single byte
    $nibble  = { E2 3? ?? C8 }           // ? = any nibble
    $not_00  = { F4 23 ~00 62 B4 }       // ~ = NOT this byte
    $jump    = { F4 23 [4-6] 62 B4 }     // [n-m] = n to m arbitrary bytes
    $unbnd   = { AA BB [10-] CC DD }     // unbounded upper limit
    $any_len = { AA BB [-] CC DD }       // 0 or more bytes
    $alt     = { F4 ( 62 B4 | 56 ) 45 } // alternatives in parentheses

Text Strings

Double-quoted strings with C-style escapes (\n, \t, \xNN, \\, \").

strings:
    $t1 = "This program cannot be run in DOS mode"
    $t2 = "NtCreateThread\x00"
    $t3 = "HKEY_CURRENT_USER\\Software\\Run"

Regular Expressions

Perl-like syntax between forward slashes. Modifiers /i (case-insensitive) and /s (dot matches newline).

strings:
    $r1 = /[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}/   // IPv4
    $r2 = /https?:\/\/[a-z0-9\-]{3,}\.(ru|cn|top|xyz)/i
    $r3 = /HKEY_[A-Z_]+/i
    $r4 = /-----BEGIN [A-Z ]+ KEY-----/s

String Modifiers

Modifiers follow the string definition, separated by spaces.

Modifier Types Effect
nocase text, regex Case-insensitive match
wide text, regex Match UTF-16 LE encoding (every char followed by \x00)
ascii text, regex Match ASCII; combine with wide to catch both encodings
fullword text, regex Only match when bounded by non-alphanumeric characters
xor text Try all XOR keys 0x00–0xFF; use xor(0x01-0xff) to skip plaintext
base64 text Match Base64-encoded variants (3 alignment permutations); min 4 chars
base64wide text Like base64 but the base64 output is then UTF-16 LE encoded
private text, hex, regex Participates in matching but never printed in output

Incompatibilities: nocase cannot be combined with xor, base64, or base64wide. fullword cannot be combined with base64 or base64wide.

strings:
    $a = "Borland" wide ascii nocase       // catch both encodings, any case
    $b = "cmd.exe" xor(0x01-0xff)          // common single-byte XOR obfuscation
    $c = "MZ" base64                       // base64-encoded PE header
    $d = /\.dll$/i fullword                // extension, word-bounded
    $e = { DE AD BE EF } private           // matches counted, never printed

Conditions

String Presence and Counting

condition:
    $a                          // true if $a matches anywhere
    $a and $b                   // both must match
    not $c                      // $c must not match
    #a >= 3                     // $a appears at least 3 times
    #b == 0                     // $b does not appear

Offsets and Lengths

    $a at 0                     // $a must start at offset 0
    $b at 0x100                 // exact offset
    $c in (0x0..0x200)          // anywhere in range
    @a                          // offset of first $a match
    @a[2]                       // offset of second match
    !a                          // length of first $a match
    !a[2]                       // length of second match
    @a - @b < 10                // within 10 bytes of each other

File Size

    filesize < 200KB
    filesize > 1MB
    filesize == 4096

Units: KB, MB. No suffix = bytes.

Raw Data Reads

Read specific bytes from the file without defining a string.

    uint16(0) == 0x5A4D           // "MZ" — PE file magic
    uint32(0) == 0x464C457F       // ELF magic (little-endian)
    uint32be(0) == 0x7F454C46     // ELF magic (big-endian)
    uint8(0) == 0x7F and
    uint8(1) == 0x45              // 'E'

Functions: int8, int16, int32, uint8, uint16, uint32; append be for big-endian.

Set Operators (of)

    any of them                   // any string defined in the rule
    all of them                   // every string must match
    none of ($b*)                 // none of the $b* group
    2 of ($a, $b, $c)             // at least 2 of these 3
    any of ($prefix*)             // any string whose name starts with $prefix
    1 of (Rule1, Rule2)           // at least 1 of these rules matches (rule references)

for..of — Per-occurrence conditions

    for all of them : ( # > 2 )           // every pattern appears 2+ times
    for any of ($a*) : ( @ > 0x100 )      // any $a* match is past offset 0x100
    for 2 of ($a, $b, $c) : ( @ < 512 )  // at least 2 of those match before byte 512

for..in — Index iteration

    for all i in (1..#a) : ( @a[i] < 100 )         // every $a match is within first 100 bytes
    for any i in (1..#a) : ( @a[i] + 10 == @b[i] ) // some $a match is 10 bytes before a $b match

String operators (v4.2.0+)

    pe.version_info["CompanyName"] contains "Microsoft"
    pe.description startswith "Windows"
    pe.version_info["CompanyName"] icontains "microsoft"   // case-insensitive
    pe.description matches /[Mm]icrosoft/

Worked Example: Weighted Scoring

Rather than requiring all indicators, use math.to_number to weight evidence:

import "math"

rule Suspicious_PowerShell_Downloader {
    meta:
        description = "Scores indicators; fires at >= 70 points"
    strings:
        $iex   = "IEX(" nocase
        $wget  = "New-Object Net.WebClient" nocase
        $enc   = "FromBase64String" nocase
        $bypass = "-ExecutionPolicy Bypass" nocase
    condition:
        math.to_number($iex)    * 30 +
        math.to_number($wget)   * 30 +
        math.to_number($enc)    * 25 +
        math.to_number($bypass) * 15 >= 70
}

Modules

Import a module at the top of the file:

import "pe"
import "math"

PE Module

The most commonly used module for Windows malware analysis.

import "pe"

rule Packed_PE {
    condition:
        uint16(0) == 0x5A4D and
        pe.number_of_sections < 4 and
        math.entropy(pe.sections[0].raw_data_offset,
                     pe.sections[0].raw_data_size) > 7.0
}

Key attributes:

pe.machine == pe.MACHINE_AMD64      // or MACHINE_I386, MACHINE_ARM64
pe.number_of_sections
pe.timestamp                        // Unix epoch; 0 in many stripped files
pe.entry_point
pe.is_dll()
pe.is_32bit() / pe.is_64bit()
pe.imphash()                        // normalized import hash (lowercase string)

// Sections (0-indexed array)
pe.sections[0].name                 // e.g. ".text"
pe.sections[0].virtual_address
pe.sections[0].raw_data_offset
pe.sections[0].raw_data_size
pe.sections[0].characteristics

// Imports
pe.imports("kernel32.dll", "WriteProcessMemory")   // bool
pe.imports("ntdll.dll")                            // count of ntdll imports

// Exports
pe.exports("GetProcAddress")                       // bool
pe.exports(72)                                     // by ordinal

// Version info
pe.version_info["CompanyName"] contains "Microsoft"
pe.version_info["FileDescription"] startswith "Windows"

// Characteristics
pe.characteristics & pe.DLL

ELF Module

import "elf"

elf.machine == elf.EM_X86_64
elf.machine == elf.EM_ARM
elf.machine == elf.EM_AARCH64
elf.entry_point
elf.sections[0].name
elf.sections[0].size
elf.segments[0].flags & elf.PF_X   // executable segment
elf.symtab[0].name
elf.symtab[0].type == elf.STT_FUNC
elf.telfhash()                      // TLSH of symbols

Math Module

import "math"

// Entropy: 0.0 (uniform) to 8.0 (random/encrypted)
math.entropy(0, filesize) >= 7.0

// Per-section entropy
math.entropy(
    pe.sections[0].raw_data_offset,
    pe.sections[0].raw_data_size) > 7.0

math.mean(0, filesize) < 72.0
math.mode(0, filesize) == 0xFF          // most frequent byte value (v4.2.0+)
math.count(0x00, 0, filesize) > 1000    // count zero bytes (v4.2.0+)
math.percentage(0xFF, 0, 1024) >= 0.9  // 90%+ of first 1KB is 0xFF (v4.2.0+)
math.abs(@a - @b) == 1

High entropy (≥ 7.0) in a PE section is a common packing or encryption indicator. However, high-entropy sections are also normal in legitimately compressed executables, so combine with other indicators.

Hash Module

import "hash"

hash.md5(0, filesize) == "feba6c919e3797e7778e8f2e85fa033d"
hash.sha256(0, filesize) == "abc123..."
hash.md5(0, 512)                        // hash of first 512 bytes
hash.crc32(0, filesize)

All returned hash strings are lowercase.

Time Module

import "time"

// Flag PE files with compile timestamps in the future
pe.timestamp > time.now()

// Flag suspiciously old compile timestamp (before 1990)
pe.timestamp < 631152000

Command-Line Usage

YARA (Classic)

yara [OPTIONS] <rules_file> <target>

<target> can be a file, directory, or PID (for process memory scanning).

Flag Description
-r Recurse into directories
-s Print matching strings
-m Print metadata
-e Print rule namespace
-g Print tags
-c Print count of matches only
-n Negate: print rules that did NOT match
-t <tag> Filter output to rules with this tag
-f Fast scan: stop at first match per rule
-d <var>=<val> Define external variable (repeatable)
-p <N> Use N threads for directory scanning
-a <secs> Timeout in seconds
-z <bytes> Skip files larger than N bytes
-C Treat rules file as compiled .yarc
--scan-list Read target paths from a file (one per line)
-D Print module data (debug module output)
# Scan a directory, print strings and metadata
yara -r -s -m rules.yar /path/to/samples/

# Scan a running process
yara rules.yar 4321

# Filter by tag, with external variable
yara -t ransomware -d owner="finance" rules.yar /samples/

# Compile rules and use compiled file
yarac rules.yar compiled.yarc
yara -C compiled.yarc target.bin

# Scan files listed in a file
yara --scan-list rules.yar filelist.txt

YARA-X (yr)

YARA-X uses subcommands.

yr scan

yr scan [OPTIONS] <RULES_PATH>... <TARGET_PATH>

Key flags:

Flag Description
--define <VAR=VALUE> External variable (repeatable)
-m, --print-meta Print metadata
-g, --print-tags Print tags
--print-strings[=N] Print matched patterns (optional char limit)
-r, --recursive[=DEPTH] Recurse with optional max depth
--scan-list Targets from file
--output-format <fmt> text, ndjson, or json
--profiling Identify slowest rules by performance
-C, --compiled-rules RULES_PATH is a .yarc file
--threads N Number of scanning threads
--timeout <SECS> Abort scan after N seconds
--relaxed-re-syntax Relax regex strictness (porting from YARA)
# Scan and print JSON output
yr scan --output-format json rules.yar sample.exe | jq '.matches[].rule'

# Recursive scan with profiling
yr scan --profiling -r rules/ /samples/

# Profile slowest rules to identify performance bottlenecks
yr scan --profiling rules.yar /large/corpus/

Other yr subcommands

# Compile rules to binary (faster repeated scanning)
yr compile rules.yar -o compiled.yarc

# Inspect module data without writing a rule
yr dump --module pe suspicious.exe
yr dump --output-format yaml --module lnk shortcut.lnk
yr dump --module elf malware.elf

# Auto-format rules (like gofmt for YARA)
yr fmt rules.yar
yr fmt --check rules.yar    # exit non-zero if formatting needed (CI use)

# Show dependencies and module usage
yr deps rules.yar

# Auto-fix fixable warnings
yr fix warnings rules.yar

External Variables

Define variables in rules that callers provide at scan time. Useful for context-aware rules (e.g., “is this on a production host?”).

rule CheckContext {
    condition:
        ext_hostname contains "prod" and
        ext_file_owner == "root" and
        ext_is_quarantined == false
}

Pass values on the command line:

# YARA
yara -d ext_hostname="prod-web-01" -d ext_file_owner="root" \
     -d ext_is_quarantined=false rules.yar target.bin

# YARA-X
yr scan --define ext_hostname="prod-web-01" \
        --define ext_file_owner="root" \
        --define ext_is_quarantined=false \
        rules.yar target.bin

In Python (yara-python):

rules = yara.compile('rules.yar',
    externals={'ext_hostname': 'default', 'ext_file_owner': 'unknown', 'ext_is_quarantined': False})
matches = rules.match('target.bin',
    externals={'ext_hostname': 'prod-web-01', 'ext_file_owner': 'root'})

Python API

yara-python (Classic YARA)

import yara

# Compile
rules = yara.compile('rules.yar')
rules = yara.compile(source='rule test { condition: true }')

# Match a file
matches = rules.match('/path/to/sample.exe')

# Match a process
matches = rules.match(pid=1234)

# Match raw bytes
matches = rules.match(data=b'\x4D\x5A\x00\x00...')

for match in matches:
    print(match.rule, match.tags, match.meta)
    for s in match.strings:
        print(f"  {s.identifier} at offset {s.instances[0].offset}")

yara-x Python API

The YARA-X Python API is not compatible with yara-python; code must be rewritten.

import yara_x

# Compile
rules = yara_x.compile('rule test { condition: true }')

# Or use a Compiler for multi-namespace workflows
compiler = yara_x.Compiler()
compiler.new_namespace("apt_rules")
compiler.add_source(open("apt.yar").read())
rules = compiler.build()

# Scanner
scanner = yara_x.Scanner(rules)
scanner.set_timeout(30)
result = scanner.scan_file("sample.exe")
result = scanner.scan(b"\x4D\x5A...")

# Results
for rule in result.matching_rules:
    print(rule.identifier, rule.namespace, rule.metadata)
    for pattern in rule.patterns:
        for match in pattern.matches:
            print(f"  {pattern.identifier} at offset={match.offset} len={match.length}")

Note: YARA-X does not yet support scanning process memory from Python. Use classic YARA (yara-python) for process scanning.


YARA-X: Key Differences from YARA

Breaking Changes

Most YARA 4.x rules work unchanged in YARA-X. These are the cases that require edits:

Issue YARA behavior YARA-X behavior
Regex: unescaped { Accepted silently Error — must escape as \{
Regex: invalid escape (e.g. \g) Accepted silently Error
base64 on strings < 3 chars Allowed (false positives) Error — minimum 3 chars
Wildcard rule names (1 of (rule*)) Supported Not supported
Negative array index (@a[-1]) Allowed Error
xor + fullword Surrounding bytes checked without XOR Surrounding bytes also XOR’d

Use yr scan --relaxed-re-syntax as a transitional flag when porting rules with regex issues.

New Features in YARA-X

with statement — local variables in conditions:

import "pe"

rule ReusedStrings {
    condition:
        with first = pe.version_info["CompanyName"],
             last  = pe.exports("GetProcAddress") : (
            first contains "Microsoft" and last
        )
}

.len() method on strings, arrays, and dicts:

condition:
    pe.version_info["CompanyName"].len() > 0 and
    pe.sections.len() > 5

yr dump — inspect module output without writing a rule:

yr dump --module pe malware.exe
yr dump --module lnk phishing_invoice.lnk

yr fmt — canonical rule formatter (like gofmt for YARA).

--profiling — built-in per-rule timing; identify which rules are slow.

--output-format ndjson — newline-delimited JSON for pipeline processing.

New Modules in YARA-X

Module Purpose
lnk Windows Shortcut (.lnk) files — detects phishing delivery vectors
macho macOS Mach-O binary format
dotnet .NET assembly analysis
dex Android DEX files
crx Chrome extensions (includes permhash for permission fingerprinting)
console Logging/debugging output from within rules
string String manipulation utilities

Practical Rule Writing

Anti-FP Checklist

  1. Use unique strings. Avoid strings that appear in common runtimes (OpenSSL, zlib, MSVC CRT).
  2. Minimum 6–8 chars for text patterns; shorter strings match too broadly.
  3. Combine conditions. all of ($a, $b, $c) is far more precise than any of them.
  4. Guard with file type checks. uint16(0) == 0x5A4D or uint32(0) == 0x464C457F before string scans.
  5. Test against clean corpora. Run against /usr/bin/*, Windows System32, common software before deploying.
  6. Use 2 of ($a, $b, $c, $d) when some indicators may be absent — require evidence, not perfection.

Performance Checklist

  1. Cheap conditions first. uint16(0) == 0x5A4D and filesize < 5MB and ... — header checks short-circuit before expensive string scans.
  2. Use -f / --fast-scan when presence is all you need (stops at first match).
  3. Prefer hex over regex for fixed byte sequences — YARA’s Aho-Corasick handles fixed strings fastest.
  4. Bound your jumps. [4-16] instead of [0-].
  5. Avoid nocase on wide regex — it is substantially slower.
  6. Anchor regex patterns. /^MZ/ has a better atom than /MZ/; long fixed prefixes are extracted for pre-screening.
  7. Use yr scan --profiling to find slow rules in your ruleset.
  8. Precompile with yarac / yr compile for repeated scanning.

Testing Rules

Craft minimal test files:

# Test a text pattern rule
printf 'cmd.exe /c powershell -ExecutionPolicy Bypass' > /tmp/test.bin
yara -s rules.yar /tmp/test.bin

# Test a PE header rule (fake MZ header)
python3 -c "import sys; sys.stdout.buffer.write(b'MZ' + b'\x00'*100)" > /tmp/fake.exe
yara -s rules.yar /tmp/fake.exe

Verify a rule does NOT match clean files:

yara -r rules.yar /usr/bin/ 2>/dev/null | grep -c "RuleName"   # should be 0

JSON output for scripted testing (YARA-X):

yr scan --output-format json rules.yar sample.bin | jq '.matches | length'

YARA-CI (GitHub App) runs automated tests on push and flags false positives against a whitelist corpus. Add it to any GitHub repo containing YARA rules.

Full Worked Example: PE Malware Classifier

import "pe"
import "math"

rule Potential_Packed_Downloader : suspicious {
    meta:
        author      = "CS493 Example"
        description = "PE with high entropy, no version info, and download indicators"
        severity    = "medium"

    strings:
        $url1 = /https?:\/\/[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}/ nocase
        $url2 = /https?:\/\/[a-z0-9]{8,}\.(ru|cn|top|xyz)/i
        $dl1  = "URLDownloadToFile" nocase
        $dl2  = "WinHttpOpen" nocase
        $dl3  = "InternetOpenUrl" nocase
        $enc1 = "FromBase64String" nocase
        $enc2 = "cmd.exe /c" xor(0x01-0xff)

    condition:
        uint16(0) == 0x5A4D and                              // is a PE file
        filesize < 5MB and
        pe.number_of_sections >= 2 and
        not pe.is_dll() and                                  // not a library
        math.entropy(0, filesize) >= 6.5 and                 // somewhat packed
        (pe.version_info["CompanyName"] == "" or
         not pe.imports("kernel32.dll")) and                 // stripped metadata
        (1 of ($url*) or 1 of ($dl*)) and                   // has download indicator
        (any of ($enc*))                                      // plus encoding
}

Integration

Suricata

Suricata does not run YARA rules natively — integration uses Suricata’s Lua output module to scan extracted files from network traffic.

# suricata.yaml — add to outputs
outputs:
  - lua:
      enabled: yes
      scripts:
        - yara.lua

Configure yara.lua (from the yara-suricata project) with your rules path and Suricata file-store location:

suricata_filestore     = "/var/log/suricata/files"
yara_path              = "/usr/bin/yara"
yara_rules_path        = "/usr/share/yara/rules.yar"

Suricata writes extracted files to the file-store; the Lua script calls YARA on each file and logs hits to Suricata’s JSON log. Files with no hits are deleted automatically.

CAPE Sandbox

CAPE integrates YARA at multiple points:

  1. Static pre-detonation — classifies the sample by family before execution
  2. Dynamic in-detonation — YARA rules on unpacked memory act as debugger breakpoints, firing mid-execution
  3. Payload classification — identifies malware family after unpacking
  4. Config extraction — YARA-matched signatures drive automated config extraction
  5. Anti-evasion — YARA + debugger actions detect and counter sandbox evasion in real time

Place custom .yar files in CAPE’s data/yara/ directory.

Velociraptor

Velociraptor runs YARA hunts across an entire endpoint fleet via VQL (Velociraptor Query Language):

-- Define the YARA rule
LET YaraRule = '''
rule Suspicious_LNK {
    meta:
        description = "LNK with long command line"
    strings:
        $cmd = "powershell" nocase
        $enc = "-EncodedCommand" nocase
    condition:
        any of them
}
'''

-- Hunt for matches across the fleet
SELECT FileName, String.Offset AS Offset,
       str(str=String.Data) AS Hit
FROM foreach(
  row={SELECT FullPath FROM glob(globs='''C:\Users\*\Downloads\*.lnk''')},
  query={SELECT * FROM yara(files=FullPath, rules=YaraRule)}
)

Key VQL functions:

Function Description
yara(files=PATH, rules=RULE) Scan file(s); memory-mapped without accessor for full-file analysis
proc_yara(pid=N, rules=RULE) Scan process memory
yara(files=PATH, rules=RULE, accessor="ntfs") Scan via raw NTFS accessor (bypasses file locks)

Note: When an accessor is used, Velociraptor reads in chunks — rules using filesize may not behave as expected.


References