
Ein Leitfaden zum Schreiben schneller und speicherschonender YARA-Regeln.
Das Schreiben effizienter YARA-Regeln ist entscheidend, um eine schnelle und genaue Scan-Leistung zu gewährleisten. Dieser Leitfaden enthält wichtige Prinzipien und Best Practices, die Ihnen helfen, Ihre Regeln zu optimieren, unnötige Berechnungen zu reduzieren und häufige Fallstricke zu vermeiden. Er beinhaltet Erkenntnisse von Branchenexperten, darunter Victor M. Alvarez, WXS, und Beiträge aus der YARA-Community.
Dieser Abschnitt bietet eine kurze Zusammenfassung der YARA-Performance-Best-Practices. Für ausführliche Erklärungen und Beispiele siehe den vollständigen Leitfaden weiter unten.
„Betrachten Sie YARA als einen zweistufigen Prozess: Zuerst wird nach allen in den Strings aufgeführten Mustern gesucht, und zweitens werden die Bedingungen ausgewertet. Sie können gut formulierte Bedingungen nicht nutzen, um schlecht gewählte Strings auszugleichen.“
— Wesley Shields
YARA durchläuft beim Scannen einer Datei vier Hauptschritte:
YARA sucht zuerst nach Strings, was die String-Auswahl zum wichtigsten Einzelfaktor für die Effizienz von Regeln macht.
✅ Best Practices für Strings:
\x00\x00\x00\x00 zu häufig vor.nocase sorgfältig ein – Es erzeugt exponentiell mehr Suchvarianten.YARA wertet Bedingungen sequenziell aus und stoppt beim ersten Fehlschlag.
✅ Best Practices für Bedingungen:
filesize < X) vor teuren Bedingungen.for all i in (1..filesize) ist ineffizient).@) anstelle von Regex für Sequenzprüfungen.⚠ Hinweis: Regex-Bedingungen unterstützen kein Short-Circuit und werden immer zuletzt ausgewertet.
Module wie pe, elf oder magic müssen die gesamte Datei parsen, bevor die Auswertung erfolgt, was die Scan-Zeit erhöht.
✅ Alternativen:
pe.is_pe den Ausdruck uint16(0) == 0x5A4D, um PE-Dateien zu identifizieren.Übermäßige Übereinstimmungen verlangsamen das Scannen und können „too many matches“-Fehler auslösen.
✅ Beheben ineffizienter Übereinstimmungen:
.*, .+ oder {x,} ohne obere Grenze.@herrcore hat ein hilfreiches Video-Tutorial erstellt, das die in diesem Performance-Leitfaden behandelten Themen abdeckt.
Einführung in YARA – Effiziente YARA-Regeln schreiben
Um ein besseres Verständnis dafür zu bekommen, was und wo an der YARA-Leistung optimiert werden kann, ist es hilfreich, den Scan-Prozess zu verstehen. Er ist grundsätzlich in vier Schritte unterteilt, die anhand dieser Beispielregel stark vereinfacht erklärt werden:
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
}
Dieser Schritt erfolgt vor dem eigentlichen Scan. YARA sucht nach sogenannten Atomen in den Suchstrings, um den Aho-Corasick-Automaten zu speisen. Die Details werden im Kapitel Atom erklärt, aber für jetzt reicht es zu wissen, dass sie maximal 4 Bytes lang sind und YARA sie ziemlich klug auswählt, um zu viele Übereinstimmungen zu vermeiden. In unserem Beispiel könnte YARA die folgenden 4 Atome auswählen:
<?phGETPOSTsser (aus assert)Hier hat der Scan begonnen. Die Schritte 2.-4. werden für alle Dateien ausgeführt. YARA sucht in jeder Datei nach den oben definierten 4 Atomen mithilfe eines Präfixbaums, dem sogenannten Aho-Corasick-Automaten. Alle Übereinstimmungen werden an die Bytecode-Engine übergeben.
Wenn es z. B. eine Übereinstimmung mit sser gibt, prüft YARA, ob ein a vorangestellt war und es mit einem t weitergeht. Wenn das zutrifft, wird mit dem Regex [\t ]{0,100}\( fortgefahren. Mit diesem cleveren Ansatz vermeidet YARA, mit einer langsamen Regex-Engine über die gesamten Dateien zu gehen, und wählt stattdessen bestimmte Teile zur genaueren Untersuchung aus.
Nachdem das gesamte Pattern Matching abgeschlossen ist, werden die Bedingungen geprüft.
YARA verfügt über einen weiteren Optimierungsmechanismus, um die CPU-intensive math.entropy-Prüfung aus unserer Beispielregel nur dann durchzuführen, wenn die 4 Bedingungen davor erfüllt sind. Näher erläutert wird dies im Kapitel Bedingungen und Short-Circuit-Auswertung.
Wenn die Bedingungen erfüllt sind, wird eine Übereinstimmung gemeldet. Der Scan wird mit der nächsten Datei in Schritt 2 fortgesetzt.
YARA extrahiert aus den Strings kurze Teilstrings mit einer Länge von bis zu 4 Bytes, die als „Atome“ bezeichnet werden. Diese Atome können an beliebiger Stelle innerhalb des Strings extrahiert werden, und YARA sucht beim Scannen der Datei nach diesen Atomen. Wenn es eines der Atome findet, überprüft es, ob der String tatsächlich übereinstimmt.
Betrachten Sie zum Beispiel diese Strings:
/abc.*cde/
=> mögliche Atome sind abc und cde, entweder das eine oder das andere kann verwendet werden. Das Atom abc wird derzeit bevorzugt, weil beide die gleiche Qualität haben und es das erste der beiden ist.
/(one|two)three/
=> mögliche Atome sind one, two, thre und hree. Wir können nach thre (oder hree) allein suchen oder sowohl nach one als auch nach two. Das Atom thre wird bevorzugt, weil es zu weniger potenziellen Übereinstimmungen führt als one und two (diese sind kürzer) und kein doppeltes e enthält (je eindeutiger der Buchstabe, desto besser).
YARA gibt sein Bestes, um die besten Atome aus jedem String auszuwählen, zum Beispiel:
{ 00 00 00 00 [1-4] 01 02 03 04 }
=> hier verwendet YARA das Atom 01 02 03 04, weil 00 00 00 00 zu häufig vorkommt.
{ 01 02 [1-4] 01 02 03 04 }
=> 01 02 03 04 wird gegenüber 01 02 bevorzugt, weil es länger ist.
Der wichtige Punkt ist also, dass Strings gute Atome enthalten sollten. Dies sind schlechte Strings, weil sie entweder zu kurze oder zu häufige Atome enthalten:
{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)/
Die schlechtesten Strings sind jene, die überhaupt keine Atome enthalten, wie:
/\w.*\d/
/[0-9]+\n/
Diese regulären Ausdrücke enthalten keinen festen Teilstring, der als Atom verwendet werden kann, daher müssen sie an jedem Offset der Datei ausgewertet werden, um zu sehen, ob sie dort übereinstimmen.
Eine weitere gute Empfehlung ist es, Schleifen mit zu vielen Iterationen zu vermeiden, insbesondere wenn die Anweisung innerhalb der Schleife zu komplex ist, zum Beispiel:
strings:
$a = {00 00}
condition:
for all i in (1..#a) : (@a[i] < 10000)
Diese Regel hat zwei Probleme. Erstens ist der String $a zu häufig, zweitens kann #a, weil $a zu häufig ist, zu hoch sein und tausende Male ausgewertet werden.
Diese andere Bedingung ist ebenfalls ineffizient, da die Anzahl der Iterationen von der Dateigröße abhängt, die ebenfalls sehr hoch sein kann:
for all i in (1..filesize) : ($a at i)
Vermeiden Sie die Verwendung des „magic“-Moduls, das auf der Windows-Plattform nicht verfügbar ist. Die Verwendung des „magic“-Moduls verlangsamt das Scannen, liefert aber exakte Übereinstimmungen.
Benutzerdefinierte GIF-Magic-Header-Definition:
rule gif_1 {
condition:
(uint32be(0) == 0x47494638 and uint16be(4) == 0x3961) or
(uint32be(0) == 0x47494638 and uint16be(4) == 0x3761)
}
Verwendung des „magic“-Moduls:
import "magic"
rule gif_2 {
condition:
magic.mime_type() == "image/gif"
}
Vermeiden Sie die Definition zu kurzer Strings. Jeder String mit weniger als 4 Bytes wird wahrscheinlich in vielen Dateien oder als gleichförmiger Inhalt in einer XORed-Datei vorkommen.
Manche Strings sind lang genug, sollten aber aus einem anderen Grund nicht verwendet werden – Gleichförmigkeit. Hier sind einige Beispiele für Strings, die nicht verwendet werden sollten, da sie zu viele Übereinstimmungen in Dateien verursachen könnten.
$s1 = "22222222222222222222222222222222222222222222222222222222222222"
$s2 = "\x00\x20\x00\x20\x00\x20\x00\x20\x00\x20\x00\x20\x00\x20" // wide formatted spaces
Die Fehlermeldung würde wie folgt aussehen:
error scanning yara-killer.dat: string "$mz" in rule "shitty_mz" caused too many matches
Versuchen Sie, String-Definitionen so eng wie möglich zu beschreiben. Vermeiden Sie nach Möglichkeit das Attribut „nocase“, da viele Atome generiert und durchsucht werden (höhere Speichernutzung, mehr Iterationen). Denken Sie daran, dass ohne Modifikatoren standardmäßig „ascii“ angenommen wird. Die möglichen Kombinationen sind:
NIEDRIG – Es wird nur ein Atom generiert.
$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
HOCH – Alle Kombinationen aus Groß- und Kleinbuchstaben für die von YARA ausgewählten 4 Bytes werden als Atome generiert.
$s5 = "cmd.exe" nocase (all different cases, e.g. "Cmd.", "cMd.", "cmD." ..)
Wenn Sie Skriptbefehle abgleichen möchten, prüfen Sie, ob die Sprache überhaupt case-insensitiv ist (z. B. PHP, Windows-Batch), bevor Sie nocase verwenden. Wenn Sie nur eine andere Groß-/Kleinschreibung für ein oder zwei Buchstaben benötigen, sind Sie mit einem Regex besser beraten, z. B.:
$re = /[Pp]assword/
Seien Sie vorsichtig bei der Arbeit mit Alternationen wie:
$re = /(a|b)cde/
$hex = {C7 C3 00 (31 | 33)}
Diese Strings erzeugen kurze Atome, die das Scannen verlangsamen können. In Fällen, in denen es nur wenige Varianten gibt, wird empfohlen, die Strings getrennt zu schreiben:
$re1 = /acde/
$re2 = /bcde/
$hex1 = {C7 C3 00 31}
$hex2 = {C7 C3 00 33}
Verwenden Sie reguläre Ausdrücke nur, wenn es notwendig ist. Die Auswertung von regulären Ausdrücken ist grundsätzlich langsamer als einfaches String-Matching und verbraucht eine erhebliche Menge an Speicher. Verwenden Sie sie nicht, wenn Hex-Strings mit Sprüngen und Wildcards das Problem lösen können.
Wenn Sie reguläre Ausdrücke verwenden müssen, vermeiden Sie gierige .* und auch widerwillige Quantifizierer .*?. Verwenden Sie stattdessen exakte Zahlen wie .{1,30} oder sogar .{1,3000}. Vergessen Sie außerdem nicht die obere Grenze (vermeiden Sie z. B. .{2,}).
Bei der Verwendung von Quantifizierern können zwei Situationen auftreten:
Wenn der Anfang des regulären Ausdrucks an einer Position verankert ist und nur das Suffix variieren kann, wird YARA die längstmögliche Übereinstimmung finden. In Fällen wie .* und .+ oder .{2,} kann dies zu langen Strings führen und das Scannen verlangsamen.
Wenn es mehrere mögliche Anfänge des regulären Ausdrucks gibt, wird YARA alle davon abgleichen.
$re1 = /Tom.{0,2}/ // will find Tomxx in "Tomxx"
$re2 = /.{0,2}Tom/ // will find Tom, xTom, xxTom in "xxTom"
Die Anzahl der kürzeren Übereinstimmungen kann leicht die Grenze überschreiten und einen „too many matches“-Fehler erzeugen.
Das folgende Beispiel ist der reguläre Ausdruck für eine E-Mail-Adresse. Bei Verwendung von [-a-z0-9._%+] mit Quantifizierern wird YARA eine Adresse mehrfach abgleichen, was nicht ideal ist. In diesem Fall wird empfohlen, eine vernünftig kleine Teilmenge von Adressen zu finden, die genügend Informationen für die Analyse liefert.
VERWENDEN
/[-a-z0-9._%+]@[-a-z0-9.]{2,10}\.[a-z]{2,4}/
OR
/@[-a-z0-9.]{2,10}\.[a-z]{2,4}/
VERMEIDEN
/[-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}/
Wenn Sie sicherstellen möchten, dass z. B. auf exec ein /bin/sh folgt, können Sie die vom @-Symbol gelieferten Offsets verwenden. Dies wäre die langsame Regex-Version:
$ = /exec.*\/bin\/sh/
Dies ist der schnellere Weg über Offsets:
strings:
$exec = "exec"
$sh = "/bin/sh"
conditions:
$exec and $sh and
@exec < @sh
Versuchen Sie außerdem, lange String-Sequenzen einzubeziehen, die als Anker im Abgleichprozess dienen können. Auch hier gilt: Je länger, desto besser.
SCHLECHT
$s1 = /http:\/\/[.]*\.hta/ // greedy [.]*
BESSER
$s1 = /http:\/\/[a-z0-9\.\/]{3,70}\.hta/ // better, with an the upper bound
AM BESTEN
$s1 = /mshta\.exe http:\/\/[a-z0-9\.\/]{3,70}\.hta/
Fehler „Too many matches“ werden durch zu allgemeine Strings verursacht, die im Eingabematerial zu häufig vorkommen, oder dadurch, dass YARA eine Instanz mehrfach abgleicht.
Ein verlangsamtes Scannen wird durch Strings verursacht, die zu kurze Atome oder gar keine erzeugen. Infolgedessen verwendet YARA einen naiven Pattern-Matching-Algorithmus, was die Verlangsamung verursacht.
Beide Probleme können in einigen Fällen durch die folgenden Schritte behoben werden:
.* und .+, .*?x{14,}Hinweis: Im nächsten Kapitel Bedingungen und Short-Circuit-Auswertung werden einige Tipps für Bedingungen erwähnt. Diese Änderungen werden jedoch die Fehler „too many matches“ und das verlangsamte Scannen nicht beheben.
Versuchen Sie, Bedingungsanweisungen so zu schreiben, dass die Elemente, die am wahrscheinlichsten „False“ ergeben, zuerst platziert werden. Die Bedingung wird von links nach rechts ausgewertet. Je früher die Engine erkennt, dass eine Regel nicht erfüllt ist, desto eher kann sie die aktuelle Regel überspringen und die nächste auswerten. Die Geschwindigkeitsverbesserung durch diese Art der Anordnung der Bedingungsanweisungen hängt vom Unterschied der erforderlichen CPU-Zyklen zur Verarbeitung der einzelnen Anweisungen ab. Wenn alle Anweisungen mehr oder weniger gleich teuer sind, bewirkt eine Umordnung keine spürbare Verbesserung. Wenn eine der Anweisungen sehr schnell verarbeitet werden kann, wird empfohlen, sie zuerst zu platzieren, um die teure Auswertung der Anweisung in Fällen zu überspringen, in denen die erste Anweisung FALSE ist.
Eine Änderung der Reihenfolge in der folgenden Anweisung führt zu keiner signifikanten Verbesserung:
$string1 and $string2 and uint16(0) == 0x5A4D
Wenn sich die Ausführungszeit der Anweisungen jedoch stark unterscheidet, verbessert eine Umordnung zur Auslösung des Short-Circuit die Scan-Geschwindigkeit erheblich:
LANGSAM
// EXPENSIVE and CHEAP
math.entropy(0, filesize) > 7.0 and uint16(0) == 0x5A4D
SCHNELL
// CHEAP and EXPENSIVE
uint16(0) == 0x5A4D and math.entropy(0, filesize) > 7.0
Die Short-Circuit-Auswertung wurde eingeführt, um teure Anweisungen zu optimieren, insbesondere „for“-Anweisungen. Manche Leute verwendeten Bedingungen wie die im folgenden Beispiel:
strings:
$mz = "MZ"
...
condition:
$mz at 0 and for all i in (1..filesize) : ( whatever )
Da die Dateigröße eine sehr große Zahl sein kann, kann „whatever“ sehr oft ausgeführt werden, was die Ausführung verlangsamt. Mit der Short-Circuit-Auswertung wird die „for“-Anweisung nun nur dann ausgeführt, wenn der erste Teil der Bedingung erfüllt ist. Diese Regel wird also nur für MZ-Dateien langsam sein. Eine zusätzliche Verbesserung könnte sein:
$mz at 0 and filesize < 100KB and for all i in (1..filesize) : ( whatever )
Auf diese Weise wird eine obere Grenze für die Anzahl der Iterationen festgelegt.
Seit Version 3.10 wurden auch Schleifen über Integer-Bereiche optimiert:
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.
Leider funktioniert dies nicht mit regulären Ausdrücken, da sie alle zunächst in die String-Matching-Engine eingespeist werden. Das folgende Beispiel verlangsamt die Suche für jede Datei und nicht nur für diejenigen mit einer Dateigröße von weniger als 200 Bytes:
strings:
$expensive_regex = /\$[a-z0-9_]+\(/ nocase
conditions:
filesize < 200 and
$expensive_regex
Diese „Short-Circuit“-Auswertung wird seit YARA-Version 3.4 angewendet.
Alle Daten im Metadatenabschnitt werden von YARA in den RAM gelesen. (Sie können dies leicht testen, indem Sie 100.000 Hashes in eine Regel einfügen und die RAM-Nutzung des YARA-Scans vorher und nachher überprüfen.) Natürlich möchten Sie die Metadaten nicht dauerhaft aus den Regeln entfernen, aber wenn Sie wenig RAM haben, könnten Sie in Ihrem Workflow unmittelbar vor dem Scannen einige unnötige Teile davon entfernen.