Lesson 6 of 9 · 28m
YARA Rules That Don't Page Anyone at 3 a.m.
Choosing strings that are actually unique, conditions that anchor scope, wildcarding byte patterns to survive rebuilds, and the testing that earns an IR team's trust.
Every YARA rule is a bet: these bytes appear in this family and nowhere else. Lose the bet one way and the rule misses next week's build; lose it the other and it matches a signed vendor binary on 30,000 endpoints at 3 a.m. I watched a rule built on "Mozilla/5.0" and a common API name quarantine half of Program Files. The analyst was technically right — the malware contained both strings. So did everything else.
Anatomy of a rule worth deploying
import "pe"
rule MAL_Kestrel_Loader_Unpacked_Mar24
{
meta:
description = "Kestrel loader, unpacked payload: XOR config loop + campaign mutex (CASE-2291)"
scope = "unpacked payload / process memory — will not match the packed dropper"
hash = "c1d84e07b3a9f62e5d1c8b4a7f3e09d26b5c4a1e8f7d3b2c0a9e6f5d4c3b2a10"
strings:
$mutex = "Kx7_SvcUpd_7731" ascii wide
$path = "/api/v2/gate.php" ascii
// movzx eax,byte [esi+ecx] / xor al,5Ah / mov [edi+ecx],al / inc ecx / cmp ecx,edx / jb
$decrypt = { 0F B6 04 0E 34 5A 88 04 0F 41 3B CA 72 F2 }
condition:
filesize < 2MB and ( $decrypt or 2 of ($mutex, $path) )
}No MZ anchor — this rule targets memory dumps too, and scope says so in plain words.
Picking strings
Good: unique mutexes, custom protocol markers, typos the developer never fixed, PDB paths (C:\Users\dev\kestrel\Release\ldr.pdb), decryption loops. Bad: API names, common user agents, library strings (OpenSSL, zlib, the This program cannot be run in DOS mode stub), and anything from the packer stub unless you're deliberately tracking the crypter.
Wildcard the parts that change
Next build, the key becomes 0x3C and the compiler picks a different register for the compare. Keep the algorithm's shape; wildcard the configuration and register bytes:
$decrypt = { 0F B6 04 0E 34 ?? 88 04 0F 41 3B ?? 72 ?? }Anchor the condition
- File type:
uint16(0) == 0x5A4D and uint32(uint32(0x3C)) == 0x00004550for on-disk PEs. - Size:
filesize < 2MBcheaply rejects most of a disk. - pe module:
pe.imphash() == "...",pe.exports("DllRegisterServer"),pe.number_of_signatures == 0.
Test like your name is on it
yara -s -r rules/kestrel.yar samples/kestrel/ # must hit every known sample; -s shows which strings fired
yara -c -r rules/kestrel.yar /mnt/goodware/ # -c prints only the match count — it must be 0The goodware corpus should look like your fleet: system directories, Office, browsers, the EDR agent, the line-of-business apps nobody remembers installing.
Field notes
- If YARA warns a string "slows down scanning", believe it — short strings, leading wildcards, and
.*regexes have no good atoms and get checked everywhere. nocaseandwidemultiply the atoms searched; use them when the sample needs them, not by habit.- A memory-scoped rule returning zero hits on disk scans means nothing. Say the scope, or IR reads silence as all-clear.
- "Tested against 50 GB of goodware, 0 hits; hits 14/14 known samples" is the sentence that gets your rule deployed.