| name | ti-yara-hunting |
| description | YARA rule writing from behavioral observations and TI report analysis — sample-to-rule conversion, condition optimization, performance tuning, and retrohunting on VirusTotal and ANY.RUN. Covers YARA/YARA-X syntax, yarGen automated generation, and production rule deployment. |
| allowed-tools | Bash Read Write |
| metadata | {"subdomain":"analyst","when_to_use":"yara, yara rule, yara-x, yargen, retrohunt, retrohunting, virustotal hunting, anyrun yara search, malware signature, malware hunting, yara condition, yara strings, threat hunting rules, livehunt","tags":"yara, hunting, threat-intelligence, retrohunt, virustotal, anyrun, malware-detection, yargen","mitre_attack":"T1588.005"} |
YARA Hunting
Write YARA rules from malware samples, behavioral observations, and threat intelligence reports. Deploy rules for retrohunting across VirusTotal (500M+ files) and ANY.RUN (2TB malware corpus) to discover campaign variants, earlier versions, and related infrastructure.
Quick Reference
apt-get install -y yara || pip install yara-python
cargo install yara-x
git clone https://github.com/Neo23x0/yarGen.git /opt/yarGen
cd /opt/yarGen && pip install -r requirements.txt
python3 yarGen.py --update
yara /workspace/rules/suspect.yar /workspace/samples/
yarac /workspace/rules/*.yar /workspace/rules/compiled.yarc
yara /workspace/rules/compiled.yarc /workspace/samples/
yr check /workspace/rules/suspect.yar
MITRE ATT&CK Mapping
| Technique | ID | Relevance |
|---|
| Obtain Capabilities: Exploits | T1588.005 | YARA identifies exploit tools/payloads in attacker arsenals |
| Develop Capabilities: Malware | T1587.001 | Rules detect custom malware based on unique strings/structures |
| Gather Victim Host Information: Software | T1592.002 | YARA scans reveal installed malware on victim hosts |
| Indicator Removal on Host | T1070 | Retrohunting finds samples attackers attempted to erase |
1. YARA Rule Syntax
Minimal Rule Structure
rule Malware_FamilyName_Variant {
meta:
author = "Decepticon TI"
date = "2025-01-01"
description = "Detects FamilyName variant based on unique strings"
hash = "e3b0c44298fc1c149afbf4c8996fb924..."
reference = "https://report-url"
tlp = "WHITE"
strings:
$s1 = "unique_string_from_sample" ascii wide
$s2 = { 4D 5A 90 00 03 00 00 00 } // MZ header + specific bytes
$s3 = /https?:\/\/[a-z0-9\-\.]+\/gate\.php/ nocase
condition:
uint16(0) == 0x5A4D and // PE file check
filesize < 5MB and
2 of ($s*)
}
String Types
strings:
// Text strings
$text1 = "CreateRemoteThread" // ASCII exact
$text2 = "CreateRemoteThread" ascii wide // Both encodings
$text3 = "http://c2.evil.com" nocase // Case insensitive
$text4 = "cmd /c" fullword // Word boundary match
// Hex strings (byte patterns)
$hex1 = { 48 8B 05 ?? ?? ?? ?? } // ?? = any byte (wildcard)
$hex2 = { 4D 5A [0-200] 50 45 00 00 } // Jump 0-200 bytes
$hex3 = { ( 74 | 75 ) 0? } // Alternation: je or jne
// Regex strings
$re1 = /https?:\/\/\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}(:\d+)?\/[a-z]+\.php/
$re2 = /[A-Za-z0-9+\/]{50,}={0,2}/ // Base64 blob
// XOR-encoded strings (YARA 4.0+)
$xor1 = "This program cannot" xor // All single-byte XOR keys
$xor2 = "config_url" xor(0x01-0xFF) // Specific XOR range
// Base64-encoded strings (YARA 4.0+)
$b64 = "powershell" base64 base64wide
Condition Essentials
condition:
// File type guards (always lead with these)
uint16(0) == 0x5A4D // PE (MZ header)
uint32(0) == 0x464C457F // ELF
uint16(0) == 0x4B50 // ZIP/DOCX/XLSX (PK)
// String matching
all of them // Every string must match
any of them // At least one
2 of ($s*) // 2+ from $s group
3 of ($api*) and 1 of ($str*) // Mixed groups
#s1 > 5 // $s1 appears 5+ times
// Size + offset constraints
filesize < 1MB
$mz at 0 // $mz must be at offset 0
$s1 in (0..1024) // $s1 in first 1KB
// PE module (import pe)
pe.imports("kernel32.dll", "VirtualAlloc")
pe.number_of_sections > 6
// Math module (import math)
math.entropy(0, filesize) > 7.0 // High entropy (packed)
// Typical combined condition
uint16(0) == 0x5A4D and filesize < 2MB and
(2 of ($s*) or all of ($api*))
2. Writing Rules from Malware Samples
Step-by-Step: Sample to Rule
strings -n 6 /workspace/samples/<SAMPLE> > /workspace/rules/strings_raw.txt
strings -n 6 -el /workspace/samples/<SAMPLE> >> /workspace/rules/strings_raw.txt
grep -iP '(http|ftp|\.php|\.aspx|mutex|pdb|\\\\Users|HKEY_|config|beacon|payload)' \
/workspace/rules/strings_raw.txt | sort -u > /workspace/rules/strings_interesting.txt
xxd /workspace/samples/<SAMPLE> | head -50
xxd -s 0x400 -l 256 /workspace/samples/<SAMPLE>
python3 -c "
import pefile
pe = pefile.PE('/workspace/samples/<SAMPLE>')
for entry in pe.DIRECTORY_ENTRY_IMPORT:
dll = entry.dll.decode()
for imp in entry.imports:
name = imp.name.decode() if imp.name else str(imp.ordinal)
print(f'{dll}:{name}')
" > /workspace/rules/imports.txt
python3 -c "
import pefile, math
pe = pefile.PE('/workspace/samples/<SAMPLE>')
for s in pe.sections:
name = s.Name.rstrip(b'\x00').decode(errors='replace')
data = s.get_data()
if data:
ent = sum(-p*math.log2(p) for p in [data.count(bytes([b]))/len(data) for b in range(256)] if p > 0)
print(f'{name}: entropy={ent:.2f} size={len(data)}')
"
Template: Rule from Behavioral Observations
rule APT_Backdoor_ObservedBehavior {
meta:
author = "Decepticon TI"
date = "2025-01-01"
description = "Backdoor observed during engagement — drops to %TEMP%, "
"contacts C2 over HTTPS, exfiltrates via DNS TXT"
tlp = "AMBER"
strings:
// Observed C2 communication pattern
$c2_uri = "/api/v1/check" ascii
$c2_ua = "Mozilla/5.0 (compatible; MSIE 10.0)" ascii
// Observed mutex (from dynamic analysis)
$mutex = "Global\\{8A4E2C-" ascii
// Observed file drops
$drop1 = "\\AppData\\Local\\Temp\\svchost32.exe" ascii wide
$drop2 = "\\ProgramData\\Microsoft\\updater.dll" ascii wide
// Observed registry persistence
$reg = "Software\\Microsoft\\Windows\\CurrentVersion\\Run" ascii wide
// DNS exfiltration pattern (hex-encoded subdomain)
$dns_exf = /[0-9a-f]{16,}\.data\.[a-z0-9\-]+\.(com|net|org)/
// Encryption routine bytes (from disassembly)
$crypto = { 8B 45 ?? 33 45 ?? 89 45 ?? 8B 4D ?? 03 4D ?? }
condition:
uint16(0) == 0x5A4D and
filesize < 3MB and
($mutex or $crypto) and
2 of ($c2_*, $drop*, $reg, $dns_exf)
}
3. Automated Rule Generation with yarGen
python3 /opt/yarGen/yarGen.py \
-m /workspace/samples/<SAMPLE> \
-o /workspace/rules/yargen_output.yar \
--excludegood
python3 /opt/yarGen/yarGen.py \
-m /workspace/samples/campaign_x/ \
-o /workspace/rules/campaign_x.yar \
--excludegood \
-a "Decepticon TI" \
-r "https://engagement-reference"
python3 /opt/yarGen/yarGen.py \
-m /workspace/samples/<SAMPLE> \
-o /workspace/rules/yargen_ai.yar \
--ai
python3 /opt/yarGen/yarGen.py \
-m /workspace/samples/<SAMPLE> \
-o /workspace/rules/yargen_output.yar \
--inverse --excludegood
yarGen Output Review Checklist
- Remove generic strings —
"Microsoft", "Windows", "kernel32.dll" add nothing
- Keep unique artifacts — PDB paths, mutex names, custom headers, C2 URIs
- Add file type guard —
uint16(0) == 0x5A4D for PE, uint32(0) == 0x464C457F for ELF
- Add size constraint —
filesize < 10MB prevents scanning huge archives
- Tighten condition — change
all of them to N of ($s*) to tolerate variants
- Test against goodware — scan
/usr/bin/, C:\Windows\System32\ for false positives
4. Condition Optimization and Performance
- Always lead with cheap checks:
uint16(0) == 0x5A4D and filesize < 5MB and ...
- Anchor strings to offsets:
$mz at 0, $config in (filesize - 4096 .. filesize)
- Bound regex:
/.{0,1000}evil/ causes backtracking; use /evil[a-z]{0,20}\.php/
- Prefer hex over regex for bytes:
{ 4D 5A [0-500] 50 45 } > /\x4D\x5A.{0,500}\x50\x45/
- Threshold matching:
3 of ($s*) instead of all of them — tolerates variant mutations
- PE module for imports:
pe.imports("ws2_32.dll", "connect") beats string-matching DLL names
yara -r /workspace/rules/suspect.yar /usr/bin/ 2>/dev/null | wc -l
yr scan --profile /workspace/rules/suspect.yar /workspace/samples/
5. TI Report-to-YARA Conversion
Use the ti-ioc-extraction skill to extract IOCs from reports, then build YARA:
- Hashes →
meta: section (for reference/correlation, not detection)
- Domains/URLs → text strings with
ascii nocase
- File paths → text strings with
ascii wide (escape backslashes)
- Snort/Suricata content matches → YARA text strings (e.g.,
content:"/gate.php" → $uri = "/gate.php" ascii)
6. Retrohunting Workflows
VirusTotal Retrohunt
VT_API_KEY="<API_KEY>"
curl -s -X POST "https://www.virustotal.com/api/v3/intelligence/retrohunt_jobs" \
-H "x-apikey: $VT_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"data": {
"type": "retrohunt_job",
"attributes": {
"rules": "rule test { strings: $s = \"unique_marker\" condition: $s }",
"notification_email": "analyst@example.com",
"corpus": "main",
"time_range": { "start": 1700000000, "end": 1710000000 }
}
}
}' > /workspace/hunting/retrohunt_job.json
JOB_ID=$(python3 -c "import json; print(json.load(open('/workspace/hunting/retrohunt_job.json'))['data']['id'])")
curl -s "https://www.virustotal.com/api/v3/intelligence/retrohunt_jobs/$JOB_ID" \
-H "x-apikey: $VT_API_KEY" | python3 -m json.tool
curl -s "https://www.virustotal.com/api/v3/intelligence/retrohunt_jobs/$JOB_ID/matching_files?limit=40" \
-H "x-apikey: $VT_API_KEY" > /workspace/hunting/retrohunt_matches.json
VirusTotal Retrohunt Limits:
- Corpus: 500M+ files (~680TB), scanned in 2-3 hours
- Hunting Pro: 12-month lookback | Standard: 3-month lookback
- Max 300 rules per job, total rule text < 1MB
- Max 10,000 matches per job
- LiveHunt (real-time): rules tested against every new upload
VirusTotal LiveHunt (Real-Time)
curl -s -X POST "https://www.virustotal.com/api/v3/intelligence/hunting_rulesets" \
-H "x-apikey: $VT_API_KEY" \
-H "Content-Type: application/json" \
-d '{"data":{"type":"hunting_ruleset","attributes":{"name":"Campaign_X_Tracker","enabled":true,"rules":"rule Campaign_X { strings: $c2 = \"evil-c2.example.com\" condition: uint16(0) == 0x5A4D and $c2 }","notification_emails":["analyst@example.com"]}}}'
ANY.RUN YARA Search
curl -s "https://api.any.run/v1/yara/search" \
-H "Authorization: API-Key <ANYRUN_API_KEY>" \
-H "Content-Type: application/json" \
-d '{
"rule": "rule test { strings: $s = \"unique_marker\" ascii condition: $s }"
}' > /workspace/hunting/anyrun_yara_results.json
Retrohunting End-to-End
yara /workspace/rules/campaign.yar /workspace/samples/
yara /workspace/rules/campaign.yar /usr/bin/ 2>/dev/null
7. YARA-X Differences
YARA-X (Rust rewrite) offers stricter parsing, better performance, and multi-line metadata (0.4.0+). Key commands:
cargo install yara-x
yr check /workspace/rules/*.yar
yr scan /workspace/rules/*.yar /workspace/samples/
If classic YARA accepts a rule but YARA-X rejects it, fix the rule — the stricter parser catches real syntax bugs.
Tools & Resources
| Tool | Purpose | Install/URL |
|---|
| yara | Rule scanning engine | apt install yara |
| yarac | Compile rules to binary | Ships with yara |
| YARA-X | Rust rewrite — stricter, faster | cargo install yara-x |
| yarGen | Auto-generate rules from samples | github.com/Neo23x0/yarGen |
| yarGen-Go | Go rewrite of yarGen | github.com/Neo23x0/yarGen-Go |
| yara-python | Python bindings | pip install yara-python |
| VT Retrohunt | Scan 500M+ historical files | virustotal.com (paid) |
| VT LiveHunt | Real-time rule matching on uploads | virustotal.com (paid) |
| ANY.RUN YARA Search | Scan 2TB malware corpus | yara.any.run |
| YARA rules repo (ANY.RUN) | Community detection rules | github.com/anyrun/YARA |
| awesome-yara | Curated rule/tool list | github.com/InQuest/awesome-yara |
Detection Signatures
| Rule Pattern | Detects | False Positive Risk |
|---|
uint16(0) == 0x5A4D and pe.imports("ntdll.dll", "NtCreateThreadEx") | Process injection via direct syscall | Low — legitimate use rare |
$s xor(0x01-0xFF) | Single-byte XOR-encoded strings | Medium — scan time increases |
math.entropy(0, filesize) > 7.5 | Packed/encrypted payloads | Medium — compressed archives also match |
pe.number_of_signatures == 0 and pe.timestamp > X | Unsigned recent PE | High — many legitimate unsigned binaries |
filesize < 50KB and pe.imports("ws2_32.dll") | Small network-capable binary | Low — shellcode droppers are typically small |
Error Handling & Edge Cases
| Problem | Cause | Solution |
|---|
| Rule compiles but matches nothing | Strings not present in target encoding | Add ascii wide to text strings; check endianness for hex |
| Too many false positives | Overly generic strings | Add file type guard, size constraint, and tighten to N of ($s*) |
| yarGen produces 100+ string rule | Default behavior — includes everything | Prune to 5-15 high-confidence strings; add manual conditions |
| Retrohunt job times out | Rule too complex or regex too broad | Simplify regex; add filesize < guard; split into multiple jobs |
| YARA-X rejects valid YARA rule | Stricter parser | Fix deprecated syntax (e.g., unescaped special chars in regex) |
Hex pattern ?? matches too broadly | Wildcards without anchoring | Add surrounding fixed bytes; use [N-M] jumps instead |
| PE module unavailable in sandbox | YARA compiled without PE module | Use uint16(0) == 0x5A4D as fallback; check yara --help for modules |
| LiveHunt notifications delayed | VT processing backlog | Normal during high-volume periods; check dashboard |
| Rule works locally but not on VT | Module version mismatch | Check VT supported YARA version and modules |
Decision Gate
IF you have a malware sample on disk
→ Extract strings + imports → write rule manually (Section 2)
→ Then run yarGen for additional string candidates (Section 3)
IF you have a TI report with IOCs but no sample
→ Extract IOCs (ti-ioc-extraction skill) → build string-based rule (Section 5)
IF you need to find campaign variants in the wild
→ Submit rule to VT Retrohunt (historical) + ANY.RUN YARA Search (Section 6)
IF you need ongoing monitoring for new variants
→ Deploy as VT LiveHunt rule (Section 6)
IF rule produces false positives
→ Add file type check, size constraint, raise string threshold (Section 4)
IF yarGen output is too noisy
→ Prune to 5-15 unique strings, add PE/ELF header check, test against goodware
IF performance is poor on large corpus
→ Lead condition with uint16/filesize check; replace regex with hex patterns (Section 4)
IF classic YARA accepts but YARA-X rejects
→ Fix the rule — YARA-X catches real bugs; the stricter parser is correct