
Un guide sur la façon d'écrire des règles YARA rapides et économes en mémoire
Écrire des règles YARA efficaces est essentiel pour maintenir des performances de scan rapides et précises. Ce guide fournit des principes clés et des bonnes pratiques pour vous aider à optimiser vos règles, réduire les calculs inutiles et éviter les pièges courants. Il intègre des idées d'experts du secteur, notamment Victor M. Alvarez, WXS, et des contributions de la communauté YARA.
Cette section fournit un résumé concis des bonnes pratiques de performance YARA. Pour des explications détaillées et des exemples, reportez-vous au guide complet ci-dessous.
« Pensez à YARA comme à un processus en deux étapes : d'abord, rechercher tous les motifs listés dans les chaînes, puis évaluer les conditions. Vous ne pouvez pas utiliser des conditions bien formées pour compenser des chaînes mal choisies. » — Wesley Shields
YARA suit quatre étapes principales lors du scan d'un fichier :
YARA recherche d'abord les chaînes, ce qui fait de la sélection des chaînes le facteur le plus important pour l'efficacité des règles.
✅ Bonnes pratiques pour les chaînes :
\x00\x00\x00\x00 apparaît trop fréquemment.nocase avec précaution – Cela génère exponentiellement plus de variations de recherche.YARA évalue les conditions séquentiellement et s'arrête au premier échec.
✅ Bonnes pratiques pour les conditions :
filesize < X) avant les conditions coûteuses.for all i in (1..filesize) est inefficace).@) au lieu de regex pour les vérifications de séquence.⚠ Remarque : Les conditions regex ne se court-circuitent pas et sont toujours évaluées en dernier.
Les modules comme pe, elf ou magic doivent analyser l'intégralité du fichier avant l'évaluation, ce qui augmente le temps de scan.
✅ Alternatives :
pe.is_pe, utilisez uint16(0) == 0x5A4D pour identifier les fichiers PE.Des correspondances excessives ralentissent le scan et peuvent déclencher des erreurs « too many matches ».
✅ Corriger les correspondances inefficaces :
.*, .+ ou {x,} sans borne supérieure.@herrcore a créé un tutoriel vidéo utile couvrant les sujets abordés dans ce guide de performance.
Pour mieux comprendre quoi et où optimiser dans les performances de YARA, il est utile de comprendre le processus de scan. Il est essentiellement divisé en 4 étapes qui seront expliquées de manière très simplifiée à l'aide de cette règle d'exemple :
import "math"
rule example_php_webshell_rule
{
meta:
description = "Just an example php webshell rule"
date = "2021/02/16"
strings:
$php_tag = "<?php"
$input1 = "GET"
$input2 = "POST"
$payload = /assert[\t ]{0,100}\(/
condition:
filesize < 20KB and
$php_tag and
$payload and
any of ( $input* ) and
math.entropy(500, filesize-500) >= 5
}
Cette étape se déroule avant le scan proprement dit. YARA recherche des « atomes » dans les chaînes de recherche pour alimenter l'automate Aho-Corasick. Les détails sont expliqués dans le chapitre atome, mais pour l'instant il suffit de savoir qu'ils font au maximum 4 octets et que YARA les choisit assez intelligemment pour éviter trop de correspondances. Dans notre exemple, YARA pourrait choisir les 4 atomes suivants :
<?phGETPOSTsser (à partir d'assert)Ici, le scan a commencé. Les étapes 2 à 4 seront exécutées sur tous les fichiers. YARA recherchera dans chaque fichier les 4 atomes définis ci-dessus à l'aide d'un arbre préfixe appelé automate Aho-Corasick. Toutes les correspondances sont transmises au moteur de bytecode.
S'il y a par exemple une correspondance sur sser, YARA vérifiera si elle était précédée d'un a et se poursuit avec un t. Si c'est le cas, il enchaînera avec la regex [\t ]{0,100}\(. Grâce à cette approche intelligente, YARA évite d'utiliser un moteur de regex lent sur l'ensemble des fichiers et ne sélectionne que certaines parties pour un examen plus approfondi.
Après que toute la recherche de motifs est terminée, les conditions sont vérifiées.
YARA dispose d'un autre mécanisme d'optimisation pour n'effectuer la vérification math.entropy, intensive en CPU, de notre règle d'exemple que si les 4 conditions précédentes sont satisfaites. Ceci est expliqué plus en détail dans le chapitre Conditions et évaluation en court-circuit
Si les conditions sont satisfaites, une correspondance est signalée. Le scan continue avec le fichier suivant à l'étape 2.
YARA extrait des chaînes de courtes sous-chaînes pouvant aller jusqu'à 4 octets, appelées « atomes ». Ces atomes peuvent être extraits de n'importe quel endroit de la chaîne, et YARA recherche ces atomes lors du scan du fichier ; s'il en trouve un, il vérifie ensuite que la chaîne correspond effectivement.
Par exemple, considérez ces chaînes :
/abc.*cde/
=> les atomes possibles sont abc et cde, l'un ou l'autre peut être utilisé. L'atome abc est actuellement préféré car ils ont la même qualité et c'est le premier des deux.
/(one|two)three/
=> les atomes possibles sont one, two, thre et hree ; on peut rechercher thre (ou hree) seul, ou à la fois one et two. L'atome thre est préféré car il conduira à moins de correspondances potentielles que one et two (qui sont plus courts) et il ne contient pas de double e (plus la lettre est unique, mieux c'est).
YARA fait de son mieux pour sélectionner les meilleurs atomes de chaque chaîne, par exemple :
{ 00 00 00 00 [1-4] 01 02 03 04 }
=> ici, YARA utilise l'atome 01 02 03 04, car 00 00 00 00 est trop courant
{ 01 02 [1-4] 01 02 03 04 }
=> 01 02 03 04 est préféré à 01 02 car il est plus long
Le point important est donc que les chaînes doivent contenir de bons atomes. Ces chaînes sont mauvaises car elles contiennent des atomes trop courts ou trop courants :
{00 00 00 00 [1-2] FF FF [1-2] 00 00 00 00}
{AB [1-2] 03 21 [1-2] 01 02}
/a.*b/
/a(c|d)/
Les pires chaînes sont celles qui ne contiennent aucun atome, comme :
/\w.*\d/
/[0-9]+\n/
Cette expression régulière ne contient aucune sous-chaîne fixe pouvant être utilisée comme atome, elle doit donc être évaluée à chaque décalage du fichier pour voir si elle correspond.
Une autre bonne recommandation est d'éviter les boucles avec trop d'itérations, surtout si l'instruction à l'intérieur de la boucle est trop complexe, par exemple :
strings:
$a = {00 00}
condition:
for all i in (1..#a) : (@a[i] < 10000)
Cette règle présente deux problèmes. Le premier est que la chaîne $a est trop courante ; le second est que, parce que $a est trop courante, #a peut être trop élevé et être évalué des milliers de fois.
Cette autre condition est également inefficace car le nombre d'itérations dépend de filesize, qui peut être également très élevé :
for all i in (1..filesize) : ($a at i)
Évitez d'utiliser le module « magic » qui n'est pas disponible sur la plateforme Windows. L'utilisation du module « magic » ralentit le scan mais fournit des correspondances exactes.
Définition personnalisée de l'en-tête magique GIF :
rule gif_1 {
condition:
(uint32be(0) == 0x47494638 and uint16be(4) == 0x3961) or
(uint32be(0) == 0x47494638 and uint16be(4) == 0x3761)
}
Utilisation du module « magic » :
import "magic"
rule gif_2 {
condition:
magic.mime_type() == "image/gif"
}
Évitez de définir des chaînes trop courtes. Toute chaîne de moins de 4 octets apparaîtra probablement dans de nombreux fichiers OU comme contenu uniforme dans un fichier XORé.
Certaines chaînes sont assez longues mais ne devraient pas être utilisées pour une autre raison : l'uniformité. Voici quelques exemples de chaînes à ne pas utiliser car elles pourraient provoquer trop de correspondances dans les fichiers.
$s1 = "22222222222222222222222222222222222222222222222222222222222222"
$s2 = "\x00\x20\x00\x20\x00\x20\x00\x20\x00\x20\x00\x20\x00\x20" // wide formatted spaces
Le message d'erreur ressemblerait à :
error scanning yara-killer.dat: string "$mz" in rule "shitty_mz" caused too many matches
Essayez de décrire les définitions de chaînes de la manière la plus précise possible. Évitez l'attribut « nocase » si possible, car de nombreux atomes seront générés et recherchés (utilisation mémoire plus élevée, plus d'itérations). Rappelez-vous qu'en l'absence de modificateurs, « ascii » est supposé par défaut. Les combinaisons possibles sont :
FAIBLE - un seul atome est généré
$s1 = "cmd.exe" // (ascii only)
$s2 = "cmd.exe" ascii // (ascii only, same as $s1)
$s3 = "cmd.exe" wide // (UTF-16 only)
$s4 = "cmd.exe" ascii wide // (both ascii and UTF-16) two atoms will be generated
$s5 = { 63 6d 64 2e 65 78 65 } // ascii char code in hex
ÉLEVÉ - Toutes les combinaisons de lettres majuscules et minuscules pour les 4 octets choisis par YARA seront générées comme atomes
$s5 = "cmd.exe" nocase (all different cases, e.g. "Cmd.", "cMd.", "cmD." ..)
Si vous souhaitez faire correspondre des commandes de script, vérifiez si le langage est insensible à la casse (par exemple php, batch Windows) avant d'utiliser nocase. Si vous n'avez besoin que d'une casse différente pour une ou deux lettres, il est préférable d'utiliser une regex, par exemple.
$re = /[Pp]assword/
Soyez prudent lorsque vous travaillez avec des alternatives telles que :
$re = /(a|b)cde/
$hex = {C7 C3 00 (31 | 33)}
Ces chaînes génèrent des atomes courts qui peuvent ralentir le scan. Dans les cas où il y a un petit nombre de variantes, il est recommandé d'écrire la chaîne séparément :
$re1 = /acde/
$re2 = /bcde/
$hex1 = {C7 C3 00 31}
$hex2 = {C7 C3 00 33}
N'utilisez les expressions régulières que lorsque c'est nécessaire. L'évaluation des expressions régulières est intrinsèquement plus lente que la correspondance simple de chaînes et consomme une quantité importante de mémoire. Ne les utilisez pas si des chaînes hexadécimales avec sauts et caractères génériques peuvent résoudre le problème.
Si vous devez utiliser des expressions régulières, évitez les quantificateurs gourmands .* et même les quantificateurs paresseux .*?. Utilisez plutôt des nombres exacts comme .{1,30} ou même .{1,3000}. N'oubliez pas non plus la borne supérieure (évitez par exemple .{2,}).
Lorsque nous utilisons des quantificateurs, deux situations peuvent se produire :
Si le début de l'expression régulière est ancré à une position et que seul le suffixe peut varier, YARA fera correspondre la correspondance la plus longue possible. Dans des cas comme .* et .+ ou .{2,}, cela peut conduire à de longues chaînes et à des problèmes de ralentissement du scan.
S'il y a plusieurs débuts possibles pour l'expression régulière, YARA les fera correspondre tous.
$re1 = /Tom.{0,2}/ // will find Tomxx in "Tomxx"
$re2 = /.{0,2}Tom/ // will find Tom, xTom, xxTom in "xxTom"
Le nombre de correspondances plus courtes peut facilement dépasser la limite et créer une erreur « too many matches ».
L'exemple suivant est l'expression régulière pour une adresse e-mail. Lors de l'utilisation de [-a-z0-9._%+] avec des quantificateurs, YARA fera correspondre une même adresse plusieurs fois, ce qui n'est pas idéal. Dans ce cas, il est recommandé de trouver un sous-ensemble raisonnablement petit d'adresses fournissant suffisamment d'informations pour l'analyse.
À UTILISER
/[-a-z0-9._%+]@[-a-z0-9.]{2,10}\.[a-z]{2,4}/
OR
/@[-a-z0-9.]{2,10}\.[a-z]{2,4}/
À ÉVITER
/[-a-z0-9._%+]*@[-a-z0-9.]{2,10}\.[a-z]{2,4}/
/[-a-z0-9._%+]+@[-a-z0-9.]{2,10}\.[a-z]{2,4}/
/[-a-z0-9._%+]{x,y}@[-a-z0-9.]{2,10}\.[a-z]{2,4}/
Si vous voulez vous assurer, par exemple, que exec est suivi de /bin/sh, vous pouvez utiliser les décalages fournis par le symbole @. Ceci serait la version lente avec regex :
$ = /exec.*\/bin\/sh/
Voici la méthode plus rapide avec décalages :
strings:
$exec = "exec"
$sh = "/bin/sh"
conditions:
$exec and $sh and
@exec < @sh
Essayez également d'inclure de longues séquences de chaînes qui pourraient servir d'ancres dans le processus de correspondance. Encore une fois, plus c'est long, mieux c'est.
MAUVAIS
$s1 = /http:\/\/[.]*\.hta/ // greedy [.]*
MEILLEUR
$s1 = /http:\/\/[a-z0-9\.\/]{3,70}\.hta/ // better, with an the upper bound
OPTIMAL
$s1 = /mshta\.exe http:\/\/[a-z0-9\.\/]{3,70}\.hta/
Les erreurs « too many matches » sont causées par des chaînes trop générales présentes trop souvent dans l'entrée, ou par YARA qui fait correspondre une même instance plusieurs fois.
Le ralentissement du scan est causé par des chaînes qui génèrent des atomes trop courts, ou pas d'atomes du tout. En conséquence, YARA utilise un algorithme naïf de correspondance de motifs, ce qui provoque le ralentissement.
Ces deux problèmes peuvent, dans certains cas, être résolus par les étapes suivantes :
.* et .+, .*?x{14,}Notez que dans le chapitre suivant, Conditions et évaluation en court-circuit, quelques conseils pour les conditions sont mentionnés. Cependant, les modifications apportées à celles-ci ne résoudront pas les erreurs « too many matches » et de ralentissement du scan.
Essayez d'écrire des instructions de condition dans lesquelles les éléments les plus susceptibles d'être « False » sont placés en premier. La condition est évaluée de gauche à droite. Plus tôt le moteur identifie qu'une règle n'est pas satisfaite, plus tôt il peut passer à la règle suivante et l'évaluer. L'amélioration de vitesse apportée par cette façon d'ordonner les instructions de condition dépend de la différence de cycles CPU nécessaires pour traiter chacune des instructions. Si toutes les instructions sont plus ou moins aussi coûteuses, réordonner les instructions n'apporte aucune amélioration notable. Si l'une des instructions peut être traitée très rapidement, il est recommandé de la placer en premier afin d'éviter l'évaluation coûteuse dans les cas où la première instruction est FALSE.
Changer l'ordre dans l'instruction suivante n'entraîne pas d'amélioration significative :
$string1 and $string2 and uint16(0) == 0x5A4D
Cependant, si le temps d'exécution des instructions est très différent, réordonner pour déclencher le court-circuit améliorera considérablement la vitesse du scan :
LENT
// EXPENSIVE and CHEAP
math.entropy(0, filesize) > 7.0 and uint16(0) == 0x5A4D
RAPIDE
// CHEAP and EXPENSIVE
uint16(0) == 0x5A4D and math.entropy(0, filesize) > 7.0
L'évaluation en court-circuit a été introduite pour aider à optimiser les instructions coûteuses, en particulier les instructions « for ». Certaines personnes utilisaient des conditions comme celle de l'exemple suivant :
strings:
$mz = "MZ"
...
condition:
$mz at 0 and for all i in (1..filesize) : ( whatever )
Parce que filesize peut être un très grand nombre, « whatever » peut être exécuté un grand nombre de fois, ralentissant l'exécution. Désormais, avec l'évaluation en court-circuit, l'instruction « for » ne sera exécutée que si la première partie de la condition est satisfaite. Ainsi, cette règle ne sera lente que pour les fichiers MZ. Une amélioration supplémentaire pourrait être :
$mz at 0 and filesize < 100KB and for all i in (1..filesize) : ( whatever )
De cette façon, une borne supérieure au nombre d'itérations est définie.
À partir de la version 3.10, les boucles sur plages d'entiers ont également été optimisées :
for all i in (0..100): (false)
for any i in (0..100): (true)
Both of these loops will stop iterating after the first time through.
Malheureusement, cela ne fonctionne pas avec les expressions régulières car elles sont toutes initialement transmises au moteur de correspondance de chaînes. L'exemple suivant ralentira la recherche pour n'importe quel fichier, et pas seulement pour ceux dont la taille est inférieure à 200 octets :
strings:
$expensive_regex = /\$[a-z0-9_]+\(/ nocase
conditions:
filesize < 200 and
$expensive_regex
Cette évaluation « en court-circuit » est appliquée depuis la version 3.4 de YARA.
Toutes les données de la section métadonnées sont lues dans la RAM par YARA. (Vous pouvez facilement le tester en insérant 100 000 hachages dans une règle et en vérifiant l'utilisation de la RAM du scan YARA avant et après.) Bien sûr, vous ne voulez pas supprimer définitivement les métadonnées des règles, mais si vous manquez de RAM, vous pouvez supprimer certaines parties inutiles de celles-ci dans votre flux de travail juste avant le scan.