Writing Yara Rules logo

Writing Yara Rules

Community
trilwu
writing-yara-rules

Author durable YARA detection rules — meta/strings/condition anatomy, string types and modifiers, structural conditions with file magic and offsets, the PE/ELF/math/hash modules, specificity-vs-durability tuning, atom-aware performance, memory and process scanning, and YARA-X. Use when writing a YARA rule, creating a signature for a malware family or a file/memory artifact, turning IOCs or a captured sample into a detection, or hunting for a family across a corpus with Retrohunt or an on-host scanner.

Overview

Publishertrilwu
Repositorysecskills
Skill namewriting-yara-rules
Stars
144
Forks
15
Bundled files
Instructions only
LicenseMIT
Links
  • Markdown instructions

    A SKILL.md file the model loads on demand, so it only costs tokens when a request actually matches.

  • Works with any LLM

    AI skills are plain Markdown, not provider-specific code, so this works with GPT, Claude, Gemini, Grok, or a local model.

  • Self-contained

    Everything the model needs lives in the instructions — no extra files to sync.

  • Open source

    Published by trilwu on GitHub. Read the source before you install it.

Installation

Install the Writing Yara Rules AI skill in TypingMind to use it with any LLM, or drop it into another agent that reads SKILL.md.

1

Install in TypingMind

TypingMind installs a skill straight from its GitHub folder — it reads SKILL.md, bundles the resource files, and stores the result locally.

  1. Open the app and go to Plugins → Skills.
  2. Choose "Install from GitHub".
  3. Paste the skill folder URL below and confirm.
  4. Enable the skill in any chat where you want it available.
Plugins → Skills → Add skill → From GitHub URL, then paste the folder URL and press Continue.
2

Install in another agent

Any agent that reads the Agent Skills format can use this skill — copy the folder into that agent's skills directory.

Claude Code — .claude/skills
git clone --depth 1 https://github.com/trilwu/secskills.git /tmp/secskills
mkdir -p .claude/skills
cp -r /tmp/secskills/secskills-defense/skills/writing-yara-rules .claude/skills/writing-yara-rules
Restart Claude Code after copying so it picks up the new skill.

Use it in TypingMind

Enable Writing Yara Rules in any TypingMind chat and the model takes it from there. Its name and description sit in the system prompt, and the moment a request matches, the model loads the full instructions itself — you never invoke it by hand, and it costs no tokens until it is actually used.

The model loads Writing Yara Rules on its own as soon as a request matches it.

Works with any AI model

AI skills are plain Markdown instructions rather than provider-specific code, so Writing Yara Rules is not tied to the model it was written for. Install it once in TypingMind and use it with GPT-5, Claude, Gemini, Grok, DeepSeek, Mistral, Llama, or a local model you run yourself — all on your own API keys.

  • Loaded only when it is needed

    The system prompt carries just the name and description. The instructions are fetched on the first matching request, so an idle skill costs nothing.

  • Switch models mid-chat

    Because the skill is instructions rather than code, changing model does not break it — the next model reads the same SKILL.md.

Skill instructions

This is the SKILL.md content the model loads. Read it before installing — a skill is instructions your model will follow.

Writing YARA Rules

A good YARA rule matches the malware's nature — its code, its structure, its unavoidable constants — not its costume, which is a filename or a mutable string that changes on the next build. Match the nature and the rule survives recompilation and catches the whole family; match the costume and you catch one sample once. The entire craft is choosing strings and a condition that are both specific enough to avoid false positives and durable enough to generalize.

When to Use

  • Writing a YARA rule to detect a malware family, tool, or capability
  • Creating a signature for a file or in-memory artifact from a known sample
  • Turning IOCs or a captured specimen into a portable, testable detection
  • Hunting for a family across a corpus (VirusTotal Retrohunt, LOKI/THOR, on-host scan)
  • Clustering related samples by shared code, constants, or structure
  • Reviewing or tuning an existing rule for false positives and scan performance

When NOT to Use

  • The detection belongs in log or SIEM telemetry, not on files or memory — use writing-sigma-rules
  • Choosing what to detect and where it should live, the strategy above rule syntax — use engineering-detections
  • You do not yet understand the sample well enough to pick durable anchors — use analyzing-malware first
  • Running the hunt rather than authoring the signature — use hunting-threats
  • Packaging IOCs, attribution, and a finished report — use producing-threat-intelligence

Rule Anatomy

Three sections: meta (documentation, never matched on), strings (the patterns), condition (the boolean that decides a hit).

yara
import "pe"

rule Family_Loader_v1
{
    meta:
        author      = "analyst"
        date        = "2026-07-26"
        description = "ExampleLoader stage-1, config-decode stub + family constants"
        hash        = "a1b2c3...<sha256 of the analyzed sample>"
        reference   = "https://internal/case/1234"
        version     = "1"
        tlp         = "amber"
    strings:
        $decode = { 8A 04 0? 32 0? 88 0? 4? 3B ?? 72 }
        $marker = "cfg::begin" ascii
        $mutex  = "Global\\ExL-%08x" ascii
    condition:
        uint16(0) == 0x5A4D and filesize < 2MB and 2 of them
}

Meta fields that matter. author and date for provenance; hash records the exact sample the rule was derived from so a future analyst can re-verify; reference links the case or report that justifies it; description states what and why. Meta is free-form text — YARA never evaluates it — so treat it as the rule's changelog. Add version/tlp/score fields your tooling consumes.

Strings and Modifiers

Text strings are the default. Hex strings match raw bytes and support wildcards and jumps. Regex is written between slashes.

yara
strings:
    $text  = "Mozilla/5.0 (Custom-UA)" ascii wide
    // Hex: ?? = any byte, 0? = any low nibble, [4-6] = 4 to 6 arbitrary bytes
    $hex   = { 55 8B EC 83 EC ?? 53 56 57 [4-6] E8 ?? ?? ?? ?? }
    $re    = /\/gate\/[a-f0-9]{16}\.php/ nocase

Modifiers change how a string matches. Chain those that combine.

ModifierEffect
nocaseCase-insensitive text/regex match
wideMatch UTF-16LE (two bytes per char) — most Windows strings
asciiMatch single-byte form; pair with wide to match both
fullwordMatch only when bounded by non-alphanumeric bytes
xorMatch all 256 single-byte XOR-encoded variants (keys 0x00-0xFF)
xor(0x01-0xff)XOR over a bounded key range
base64 / base64wideMatch the three base64-encoded alignments
privateMatch is never shown in output (-s/callback); still counts in of them

xor finds strings hidden behind a one-byte key without knowing the key; base64 catches config values embedded in encoded blobs. Both cost scan time — use them when you have evidence the encoding is present, not by default.

Building Durable Conditions

The condition is where specificity lives. Beyond all of them / N of them:

yara
condition:
    // Anchor on file type before touching strings — cheap and decisive
    uint16(0) == 0x5A4D and              // MZ (PE)
    uint32(0) == 0x464C457F and          // 0x7F 'E' 'L' 'F' (ELF)

    // Offset constraints
    $magic at 0 and                      // string must sit at offset 0
    $tag in (0..1024) and                // within the first 1 KB
    #beacon >= 3 and                     // the $beacon string appears 3+ times

    // Bound the search — a 40-byte dropper is not a 50 MB installer
    filesize < 500KB and

    // Quorum across a candidate set, resilient to any one string changing
    3 of ($cfg*) and

    // for..of: k of a set, with a per-match predicate
    for any i in (1..#url) : ( @url[i] < 0x2000 )

#string is the match count, @string[i] the offset of the i-th match, !string[i] its length. for..of expresses "any/all/N of these strings, each satisfying a condition" — the tool for rules like "an RWX marker within 100 bytes of a known stub." Anchoring on uint* magic first lets the engine skip non-matching files before scanning strings, which is both correctness and speed.

Specificity vs. Durability

Every rule sits on a line between too-broad (false positives) and too-narrow (catches one build). Aim for a cluster of family-specific constants plus one structural anchor.

Bad — a single common string, matches half of goodware:

yara
rule Bad_TooBroad {
    strings:
        $ = "GetProcAddress"
    condition:
        $
}

Bad — a filename, the definition of a costume; the family renames it per campaign:

yara
rule Bad_Filename {
    strings:
        $ = "svch0st_updater.exe" nocase
    condition:
        $
}

Good — several constants the author cannot casually change, gated by file type and size, requiring a quorum so one mutated string does not break the rule:

yara
rule Good_FamilyCluster {
    meta:
        description = "ExampleRAT: config key schedule + mutex fmt + UA"
    strings:
        $keysched = { 66 0F 6F 05 ?? ?? ?? ?? 66 0F EF C8 66 0F 7F 4? }
        $mutexfmt = "ExR-{%04x-%04x}" ascii
        $ua       = "X-ExR-Build:" ascii wide
        $err      = "cfg decrypt failed @stage%d" ascii
    condition:
        uint16(0) == 0x5A4D and filesize < 3MB and 3 of them
}

The good rule generalizes because its anchors are things the malware needs — its own key schedule, its own mutex format, its own diagnostic strings — not things an operator flips between deployments.

Using the Modules

Modules expose parsed structure. import at the top of the file.

PE — the highest-value module for Windows samples:

yara
import "pe"
rule PE_Structural {
    condition:
        pe.is_pe and
        pe.imphash() == "d2a6...<import hash>" and       // API import fingerprint
        pe.number_of_sections == 5 and
        pe.imports("wininet.dll", "InternetOpenA") and
        pe.exports("PluginInit") and
        for any s in pe.sections : ( s.name == ".xdata" and s.raw_data_size == 0 ) and
        pe.rich_signature.length > 0 and                 // Rich header (compiler fingerprint)
        pe.timestamp > 1577836800
}

imphash clusters samples built from the same import table; the Rich header fingerprints the build toolchain and is hard to forge; pe.imports/pe.exports anchor on capability rather than incidental bytes.

ELF mirrors PE for Linux (elf.number_of_sections, elf.type, elf.symtab). math scores regions — flag packed or encrypted blobs:

yara
import "math"
rule Packed_HighEntropy {
    condition:
        uint16(0) == 0x5A4D and
        math.entropy(0x1000, filesize - 0x1000) >= 7.2 and   // near-random tail
        math.mean(0, filesize) > 100
}

hash computes digests inside the condition — useful to pin a specific embedded resource, not the whole file (a whole-file hash needs no YARA):

yara
import "hash"
rule EmbeddedResource {
    condition:
        hash.sha256(0x400, 0x200) == "e3b0c4...<digest of the fixed config blob>"
}

Targeting Position-Independent Things

Anchor on what moves with the code, not with the campaign. In rough order of durability: crypto constants (S-boxes, MD5/SHA init values, custom key schedules) → the family's own mutex/pipe/registry name templates → config markers and delimiters → C2 URI templates (/gate/, /panel/upload.php) → unique error and debug strings the developer left in → stack strings once you have deobfuscated them with floss. A filename, a compile timestamp, a campaign C2 IP, and a single common Win32 API name are not durable — they are the costume.

Killing False Positives

A rule is not done when it hits the sample; it is done when it does not hit goodware. Three levers, applied together:

  • Quorum — require N of M strings, never a lone common one. Any single string can appear in benign software; three family constants together will not.
  • File-type anchor — lead the condition with uint16(0) == 0x5A4D or the ELF magic so the rule never even scans an unrelated file type.
  • Size boundfilesize < N. A 20 KB loader is not a 200 MB game client; the bound removes whole categories of coincidental matches for free.

Testing and Validation

Validation is not optional. An untested rule is a future false-positive storm.

bash
yara -w rule.yar sample                 # -w suppresses warnings; does it fire?
yara -s -w rule.yar sample              # -s prints which strings matched, and where
yara -w -r rule.yar ./samples/family/   # -r recurse: must hit every known-true sample
yara -w -r rule.yar ./corpus/goodware/  # must produce ZERO hits — the load-bearing step
yara -C rules.yarc ...                  # compile once, scan many with the compiled form

Atoms and why 4+ byte anchors matter. YARA scans by extracting short fixed-byte substrings — atoms — from each string and searching for those first; a match on an atom triggers full verification of the string. A string whose only fixed run is 1-2 bytes (an over-wildcarded hex pattern, a two-letter text string) yields a weak atom that matches constantly, forcing verification on nearly every file and collapsing performance. Give every string at least one run of 4+ contiguous non-wildcard bytes. YARA warns on slow strings and short atoms at compile time — drop -w (which suppresses them) and treat those warnings as errors. Avoid unbounded and catastrophic regex (.*, nested quantifiers); prefer bounded classes and jumps. Profile with yara -S (--print-stats, per-rule statistics) when a ruleset drags.

Memory and Process Scanning

File rules and memory rules are not interchangeable. A rule anchored on uint16(0) == 0x5A4D and PE sections will not fire on a process where the payload is unpacked, relocated, and has no on-disk header.

bash
yara -w rule.yar mem.dmp                # scan a memory dump / minidump
yara -w rule.yar 4242                   # scan a live process — the PID is the final argument
yara -w -s rule.yar 4242                # -s prints which strings matched in the process, and where

For memory rules, drop the file-magic anchor and the filesize bound (a process has no meaningful filesize); anchor instead on the decoded strings and code that only exist after unpacking — the plaintext config, the C2 template, the stack strings assembled at runtime. Keep file-scan and memory-scan rules separate, or gate the file-only conditions so the same rule can serve both.

YARA-X: the Modern Engine

YARA-X is the Rust rewrite and the direction of travel: faster, safer, better diagnostics, and largely source-compatible. Author new rules to run under both where practical. Differences to know:

  • CLI is yr (e.g. yr scan rule.yar sample, yr compile, yr fmt to format).
  • Stricter parsing rejects some ambiguous legacy rules; the negative-index and a few deprecated constructs are gone; include and module semantics tightened.
  • Improved regex engine and clearer performance warnings; yr fmt and a language server ship in-box.
  • Some third-party modules from classic YARA are not all ported — check before relying on an exotic module.

Test the same rule under both engines when your deployment fleet is mixed (LOKI/THOR and older scanners run classic YARA; newer pipelines run YARA-X).

Organizing and Sharing Rules

  • Rule sets — one file per family or campaign, include "base.yar" to share helpers; compile a directory into a single .yarc for distribution.
  • Private rulesprivate rule never reports on its own; reference it from public rules as a reusable predicate (a shared "is a Windows PE" gate, a shared packer detector). Keeps conditions DRY and readable.
  • Tagsrule Foo : loader family_x { ... }; filter at scan time with yara --tag loader. Tag by role and family so hunts can select subsets.
  • External variablesyara -d ext=value (or -x module=path); reference ext in conditions to make one rule behave differently per environment (e.g. a filename/filepath passed by the scanning host).
  • Metadata discipline — consistent author/date/reference/hash/score fields let downstream tooling (THOR, VT, your CI) sort, score, and attribute.
yara
private rule is_pe {
    condition: uint16(0) == 0x5A4D and filesize < 10MB
}

rule Family_X_Loader : loader family_x {
    meta:
        author = "analyst"
        score  = 75
    strings:
        $a = "ExL-%08x" ascii
        $b = { 8A 04 0? 32 0? 88 0? 4? 3B ?? 72 }
    condition:
        is_pe and all of them
}

Rationalizations to Reject

  • "I hashed the sample, that's a rule." A hash matches exactly one file and nothing else. The next build has a different hash. That is an IOC, not a detection.
  • "It matched the sample, ship it." A rule untested against a goodware corpus is an unexploded false-positive storm. Scanning the sample proves nothing about what else it hits.
  • "One good string is enough." One string is one mutation away from useless, and common strings appear everywhere. Require a quorum of family constants.
  • "I'll wildcard the whole function to be safe." Over-wildcarded hex leaves no 4-byte atom, so the rule matches on a weak anchor and destroys scan performance. Keep real byte runs.
  • "The filename is distinctive, use it." Filenames are the costume — renamed per campaign at zero cost to the operator. Anchor on code and structure.
  • "It works on the file, so it works in memory." File-magic and section anchors do not exist in an unpacked process image. Memory needs its own rule.
  • "Broad now, tune later." Every noisy hit in production burns analyst trust and buries real matches. Tune before you deploy, not after.

Reading External Sources

Fetch public advisories, specifications, and vendor reports as Markdown:

bash
curl -sL "https://defuddle.md/<url>"      # scheme in the path is optional

This strips page boilerplate — roughly 78% fewer tokens on a prose page — and returns the full text rather than a summary, so you can grep it and trust a negative result.

Three things it is not for. Fetch JSON and API responses raw, because readability extraction mangles structured data. Fetch authenticated or JavaScript-rendered pages directly, because it retrieves them anonymously. And never route adversary infrastructure (phishing links, C2, malware hosting), client-owned hosts, or engagement URLs through it — the request leaves your machine to a third party, and for live adversary infrastructure it also tips off the operator.

Some sites block the extractor and return an error blob rather than the page — {"error":"Failed to fetch: 418 I'm a teapot"} from freedesktop.org, for instance. That is the fetch being refused, not the source saying the thing does not exist. Re-fetch the URL directly before drawing any conclusion from it.

References

  • writing-sigma-rules — the same craft for log and SIEM telemetry
  • engineering-detections — choosing what to detect and where it belongs
  • analyzing-malware — understanding the sample that a durable rule is built from
  • hunting-threats — running the hunt these signatures drive
  • producing-threat-intelligence — packaging IOCs, attribution, and reporting
  • reporting-security-findings — writing up what the detection found
  • YARA (VirusTotal) and YARA-X — the engines and their documentation
  • yarGen — auto-generate candidate rules and strings from samples (a starting point, never a finished rule)
  • yara-validator / yr fmt — lint and normalize rules in CI
  • LOKI and THOR (Nextron) — IOC and YARA scanners for on-host hunting
  • VirusTotal Retrohunt — run a rule across VT's corpus to find related samples

Frequently asked questions

What does the Writing Yara Rules AI skill do?

Author durable YARA detection rules — meta/strings/condition anatomy, string types and modifiers, structural conditions with file magic and offsets, the PE/ELF/math/hash modules, specificity-vs-durability tuning, atom-aware performance, memory and process scanning, and YARA-X. Use when writing a YARA rule, creating a signature for a malware family or a file/memory artifact, turning IOCs or a captured sample into a detection, or hunting for a family across a corpus with Retrohunt or an on-host scanner.

Why use Writing Yara Rules on TypingMind?

Because you install it once and use it with any model. Writing Yara Rules is plain Markdown rather than provider-specific code, so the same skill runs on GPT-5, Claude, Gemini, Grok, or a local model — and you can switch model mid-chat without it breaking. TypingMind runs on your own API keys, so you pay providers directly instead of a per-seat subscription, and your skills and chats stay in your own storage.

How do I install Writing Yara Rules in TypingMind?

Open Plugins → Skills → Install from GitHub in TypingMind and paste https://github.com/trilwu/secskills/tree/main/secskills-defense/skills/writing-yara-rules. TypingMind reads its SKILL.md and installs it as a skill you can enable per chat.

Which AI models can use Writing Yara Rules?

Any model you connect in TypingMind. AI skills are plain Markdown instructions rather than provider-specific code, so GPT, Claude, Gemini, Grok, and local models can all load this skill when a request matches it.

How many AI models can I use with Writing Yara Rules?

As many as you like. As long as a model supports skills, you can use Writing Yara Rules with it — GPT, Claude, Gemini, Grok, DeepSeek, Mistral, Llama and more — all on TypingMind with your own API keys.

Is the Writing Yara Rules AI skill free?

Yes. It is published on GitHub by trilwu under the MIT license. You only pay your own AI provider for the tokens you use.

What are AI skills?

An AI skill is a reusable instruction bundle that teaches an AI model how to do one specific task. It follows the open Agent Skills format: a SKILL.md file with a name and description, plus any scripts, templates or reference files the model may need. The model reads the instructions only when your request matches the skill, so an installed skill costs nothing until it is used.

How are AI skills different from plugins or MCP servers?

A plugin or MCP server gives a model new tools to call — code that runs somewhere and returns a result. An AI skill gives the model knowledge and process instead: how to approach a task, which steps to follow, what good output looks like. Skills are plain Markdown, so they need no server, no API key and no runtime, and they work with any model.

View all

Set up your own AI workspace now

Get notified about new features and future giveaways by subscribing to our newsletter 👇