
Una guida su come scrivere regole YARA veloci ed efficienti in termini di memoria.
Scrivere regole YARA efficienti è essenziale per mantenere prestazioni di scansione rapide e accurate. Questa guida fornisce principi chiave e buone pratiche per aiutarti a ottimizzare le tue regole, ridurre i calcoli non necessari ed evitare le insidie più comuni. Include approfondimenti di esperti del settore, tra cui Victor M. Alvarez, WXS, e contributi della community YARA.
Questa sezione fornisce un riepilogo conciso delle buone pratiche sulle prestazioni di YARA. Per spiegazioni dettagliate ed esempi, fai riferimento alla guida completa qui sotto.
"Pensa a YARA come a un processo in due fasi: prima, la ricerca di tutti i pattern elencati nelle stringhe, e poi, la valutazione delle condizioni. Non puoi usare condizioni ben formate per compensare stringhe scelte male."
— Wesley Shields
YARA segue quattro passaggi principali quando scansiona un file:
YARA cerca prima le stringhe, rendendo la selezione delle stringhe il fattore singolarmente più importante per l'efficienza delle regole.
✅ Buone Pratiche per le Stringhe:
\x00\x00\x00\x00 appare troppo di frequente.nocase con cautela – Genera esponenzialmente più varianti di ricerca.YARA valuta le condizioni in sequenza e si ferma al primo insuccesso.
✅ Buone Pratiche per le Condizioni:
filesize < X) prima delle condizioni costose.for all i in (1..filesize) è inefficiente).@) invece delle regex per i controlli di sequenza.⚠ Nota: Le condizioni regex non applicano lo short-circuit e vengono sempre valutate per ultime.
Moduli come pe, elf o magic devono analizzare l'intero file prima della valutazione, aumentando il tempo di scansione.
✅ Alternative:
pe.is_pe, usa uint16(0) == 0x5A4D per identificare i file PE.I match eccessivi rallentano la scansione e possono attivare errori di "too many matches".
✅ Correggere i Match Inefficienti:
.*, .+ o {x,} senza un limite superiore.@herrcore ha creato un utile video tutorial che copre gli argomenti discussi in questa guida sulle prestazioni.
Introduzione a YARA - Scrivere Regole YARA Efficienti
Per capire meglio cosa e dove può essere ottimizzato nelle prestazioni di YARA, è utile comprendere il processo di scansione. È fondamentalmente suddiviso in 4 fasi che verranno spiegate in modo molto semplificato usando questa regola di esempio:
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
}
Questo passaggio avviene prima della scansione vera e propria. YARA cercherà i cosiddetti atoms nelle stringhe di ricerca per alimentare l'automa di Aho-Corasick. I dettagli sono spiegati nel capitolo atomo, ma per ora è sufficiente sapere che sono lunghi al massimo 4 byte e che YARA li sceglie in modo piuttosto intelligente per evitare troppi match. Nel nostro esempio, YARA potrebbe selezionare i seguenti 4 atomi:
<?phGETPOSTsser (da assert)Qui la scansione è iniziata. I passaggi 2.-4. verranno eseguiti su tutti i file. YARA cercherà in ogni file i 4 atomi definiti sopra utilizzando un albero dei prefissi chiamato automa di Aho-Corasick. Qualsiasi corrispondenza viene passata al motore bytecode.
Se, ad esempio, c'è una corrispondenza su sser, YARA verificherà se è preceduta da una a e continua con una t. Se è così, proseguirà con la regex [\t ]{0,100}\(. Con questo approccio intelligente, YARA evita di far passare un lento motore regex sull'intero file e seleziona solo alcune parti da esaminare più da vicino.
Dopo che tutto il pattern matching è stato completato, vengono verificate le condizioni.
YARA ha un altro meccanismo di ottimizzazione per eseguire il controllo ad alta intensità di CPU math.entropy della nostra regola di esempio solo se le 4 condizioni precedenti sono soddisfatte. Spiegato in maggior dettaglio nel capitolo Condizioni e Valutazione Short-Circuit
Se le condizioni sono soddisfatte, viene segnalato un match. La scansione continua con il file successivo al passaggio 2.
YARA estrae dalle stringhe brevi sottostringhe lunghe fino a 4 byte chiamate "atoms". Questi atomi possono essere estratti da qualsiasi punto della stringa e YARA li cerca durante la scansione del file; se trova uno degli atomi, verifica che la stringa corrisponda effettivamente.
Ad esempio, considera queste stringhe:
/abc.*cde/
=> i possibili atomi sono abc e cde; si può usare l'uno o l'altro. L'atomo abc è attualmente preferito perché hanno la stessa qualità ed è il primo dei due.
/(one|two)three/
=> i possibili atomi sono one, two, thre e hree; possiamo cercare solo thre (o hree), oppure sia one che two. L'atomo thre è preferito perché porterà a meno corrispondenze potenziali rispetto a one e two (che sono più corti) e non contiene la doppia e (più la lettera è unica, meglio è).
YARA fa del suo meglio per selezionare i migliori atomi da ogni stringa, per esempio:
{ 00 00 00 00 [1-4] 01 02 03 04 }
=> qui YARA usa l'atomo 01 02 03 04, perché 00 00 00 00 è troppo comune
{ 01 02 [1-4] 01 02 03 04 }
=> 01 02 03 04 è preferito rispetto a 01 02 perché è più lungo
Quindi, il punto importante è che le stringhe dovrebbero contenere buoni atomi. Queste sono stringhe pessime perché contengono atomi troppo corti o troppo comuni:
{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)/
Le stringhe peggiori sono quelle che non contengono alcun atomo, come:
/\w.*\d/
/[0-9]+\n/
Questa espressione regolare non contiene alcuna sottostringa fissa che possa essere usata come atomo, quindi deve essere valutata a ogni offset del file per vedere se corrisponde.
Un'altra buona e importante raccomandazione è evitare cicli con troppe iterazioni, specialmente se l'istruzione all'interno del ciclo è troppo complessa, per esempio:
strings:
$a = {00 00}
condition:
for all i in (1..#a) : (@a[i] < 10000)
Questa regola ha due problemi. Il primo è che la stringa $a è troppo comune; il secondo è che, poiché $a è troppo comune, #a può essere troppo alto e può essere valutata migliaia di volte.
Anche quest'altra condizione è inefficiente perché il numero di iterazioni dipende da filesize, che può essere anch'esso molto alto:
for all i in (1..filesize) : ($a at i)
Evita di usare il "modulo magic", che non è disponibile sulla piattaforma Windows. L'uso del modulo "magic" rallenta la scansione ma fornisce corrispondenze esatte.
Definizione personalizzata dell'header magic GIF:
rule gif_1 {
condition:
(uint32be(0) == 0x47494638 and uint16be(4) == 0x3961) or
(uint32be(0) == 0x47494638 and uint16be(4) == 0x3761)
}
Usando il modulo "magic":
import "magic"
rule gif_2 {
condition:
magic.mime_type() == "image/gif"
}
Evita di definire stringhe troppo corte. Qualsiasi stringa con meno di 4 byte apparirà probabilmente in molti file O come contenuto uniforme in un file XORato.
Alcune stringhe sono abbastanza lunghe ma non dovrebbero essere usate per un motivo diverso: l'uniformità. Questi sono alcuni esempi di stringhe che non dovrebbero essere usate perché potrebbero causare troppi match nei file.
$s1 = "22222222222222222222222222222222222222222222222222222222222222"
$s2 = "\x00\x20\x00\x20\x00\x20\x00\x20\x00\x20\x00\x20\x00\x20" // wide formatted spaces
Il messaggio di errore sarebbe simile a:
error scanning yara-killer.dat: string "$mz" in rule "shitty_mz" caused too many matches
Cerca di rendere le definizioni delle stringhe il più ristrette possibile. Evita l'attributo "nocase" se possibile, perché verranno generati e cercati molti atomi (maggiore utilizzo di memoria, più iterazioni). Ricorda che in assenza di modificatori, per impostazione predefinita viene assunto "ascii". Le combinazioni possibili sono:
BASSO - viene generato un solo atomo
$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
ALTO - Verranno generate come atomi tutte le combinazioni di lettere maiuscole e minuscole per i 4 byte scelti da YARA
$s5 = "cmd.exe" nocase (all different cases, e.g. "Cmd.", "cMd.", "cmD." ..)
Se vuoi trovare comandi di scripting, verifica se il linguaggio è completamente case insensitive (ad es. php, batch di Windows) prima di usare nocase. Se hai solo bisogno di una diversa capitalizzazione per una o due lettere, è meglio usare una regex, ad es.
$re = /[Pp]assword/
Fai attenzione quando lavori con alternanze come:
$re = /(a|b)cde/
$hex = {C7 C3 00 (31 | 33)}
Queste stringhe generano atomi corti che possono rallentare la scansione. Nei casi in cui c'è un piccolo numero di varianti, si consiglia di scrivere la stringa separatamente:
$re1 = /acde/
$re2 = /bcde/
$hex1 = {C7 C3 00 31}
$hex2 = {C7 C3 00 33}
Usa le espressioni regolari solo quando necessario. La valutazione delle espressioni regolari è intrinsecamente più lenta del semplice matching di stringhe e consuma una quantità significativa di memoria. Non usarle se le stringhe esadecimali con salti e caratteri jolly possono risolvere il problema.
Se devi usare espressioni regolari, evita il .* greedy e persino i quantificatori reluctant .*?. Usa invece numeri esatti come .{1,30} o anche .{1,3000}. Inoltre, non dimenticare il limite superiore (evita ad es. .{2,}).
Quando si usano i quantificatori, possono verificarsi due situazioni:
Se l'inizio dell'espressione regolare è ancorato a una posizione e può variare solo il suffisso, YARA troverà la corrispondenza più lunga possibile. In casi come .* e .+ o .{2,}, questo può portare a stringhe molto lunghe e a problemi di rallentamento della scansione.
Se ci sono più inizi possibili per l'espressione regolare, YARA troverà tutti quelli possibili.
$re1 = /Tom.{0,2}/ // will find Tomxx in "Tomxx"
$re2 = /.{0,2}Tom/ // will find Tom, xTom, xxTom in "xxTom"
Il numero di corrispondenze più corte può facilmente superare il limite e generare l'errore "too many matches".
L'esempio seguente è l'espressione regolare per un indirizzo e-mail. Quando si usa [-a-z0-9._%+] con quantificatori, YARA troverà lo stesso indirizzo più volte, il che non è ideale. In questo caso, si consiglia di individuare un sottoinsieme ragionevolmente piccolo di indirizzi che fornisca informazioni sufficienti per l'analisi.
DA USARE
/[-a-z0-9._%+]@[-a-z0-9.]{2,10}\.[a-z]{2,4}/
OR
/@[-a-z0-9.]{2,10}\.[a-z]{2,4}/
DA EVITARE
/[-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}/
Se vuoi assicurarti che, ad esempio, exec sia seguito da /bin/sh, puoi usare gli offset forniti dal simbolo @. Questa sarebbe la versione lenta con regex:
$ = /exec.*\/bin\/sh/
Questo è il metodo più veloce con gli offset:
strings:
$exec = "exec"
$sh = "/bin/sh"
conditions:
$exec and $sh and
@exec < @sh
Cerca anche di includere lunghe sequenze di stringhe che possano fungere da ancore nel processo di matching. Ancora una volta, più sono lunghe, meglio è.
PESSIMO
$s1 = /http:\/\/[.]*\.hta/ // greedy [.]*
MIGLIORE
$s1 = /http:\/\/[a-z0-9\.\/]{3,70}\.hta/ // better, with an the upper bound
OTTIMO
$s1 = /mshta\.exe http:\/\/[a-z0-9\.\/]{3,70}\.hta/
Gli errori "too many matches" sono causati da stringhe troppo generiche presenti nell'input con troppa frequenza, oppure da YARA che trova la stessa occorrenza più volte.
Il rallentamento della scansione è causato da stringhe che generano atomi troppo corti, o nessun atomo. Di conseguenza, YARA utilizza un algoritmo di pattern matching ingenuo, che causa il rallentamento.
Entrambi questi problemi possono, in alcuni casi, essere risolti con questi passaggi:
.* e .+, .*?x{14,}Nota: nel prossimo capitolo Condizioni e Valutazione Short-Circuit vengono menzionati alcuni suggerimenti per le condizioni. Tuttavia, modificarle non risolverà gli errori di troppi match e di rallentamento della scansione.
Cerca di scrivere le istruzioni della condizione in modo che gli elementi che hanno maggiori probabilità di essere "False" siano posti per primi. La condizione viene valutata da sinistra a destra. Prima il motore identifica che una regola non è soddisfatta, prima può saltare la regola corrente e valutare la successiva. Il miglioramento di velocità derivante da questo modo di ordinare le istruzioni della condizione dipende dalla differenza nei cicli di CPU necessari per elaborare ciascuna istruzione. Se tutte le istruzioni hanno più o meno lo stesso costo, riordinarle non produce alcun miglioramento evidente. Se una delle istruzioni può essere elaborata molto rapidamente, si consiglia di metterla per prima, in modo da saltare la valutazione dell'istruzione costosa nei casi in cui la prima istruzione è FALSE.
Cambiare l'ordine nella seguente istruzione non produce un miglioramento significativo:
$string1 and $string2 and uint16(0) == 0x5A4D
Tuttavia, se il tempo di esecuzione delle istruzioni è molto diverso, riordinarle per attivare lo short-circuit migliorerà significativamente la velocità di scansione:
LENTO
// EXPENSIVE and CHEAP
math.entropy(0, filesize) > 7.0 and uint16(0) == 0x5A4D
VELOCE
// CHEAP and EXPENSIVE
uint16(0) == 0x5A4D and math.entropy(0, filesize) > 7.0
La valutazione short-circuit è stata introdotta per aiutare a ottimizzare le istruzioni costose, in particolare le istruzioni "for". Alcune persone usavano condizioni come quella nell'esempio seguente:
strings:
$mz = "MZ"
...
condition:
$mz at 0 and for all i in (1..filesize) : ( whatever )
Poiché filesize può essere un numero molto grande, "whatever" può essere eseguito molte volte, rallentando l'esecuzione. Ora, con la valutazione short-circuit, l'istruzione "for" verrà eseguita solo se la prima parte della condizione è soddisfatta, quindi questa regola sarà lenta solo per i file MZ. Un ulteriore miglioramento potrebbe essere:
$mz at 0 and filesize < 100KB and for all i in (1..filesize) : ( whatever )
In questo modo viene impostato un limite superiore al numero di iterazioni.
Dalla versione 3.10, anche i cicli su intervalli interi sono stati ottimizzati:
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.
Purtroppo questo non funziona con le espressioni regolari, perché vengono tutte inizialmente inserite nel motore di matching delle stringhe. L'esempio seguente rallenterà la ricerca per qualsiasi file e non solo per quelli con filesize inferiore a 200 byte:
strings:
$expensive_regex = /\$[a-z0-9_]+\(/ nocase
conditions:
filesize < 200 and
$expensive_regex
Questa valutazione "short-circuit" viene applicata dalla versione 3.4 di YARA.
Qualsiasi dato nella sezione metadata viene letto nella RAM da YARA. (Puoi facilmente verificarlo inserendo 100.000 hash in una regola e controllando l'utilizzo della RAM della scansione YARA prima e dopo.) Naturalmente non vorrai rimuovere permanentemente i metadati dalle regole, ma se hai poca RAM, potresti rimuovere alcune parti non necessarie nel tuo flusso di lavoro, subito prima della scansione.