
Um guia sobre como escrever regras YARA rápidas e com baixo consumo de memória
Escrever regras YARA eficientes é essencial para manter um desempenho de varredura rápido e preciso. Este guia fornece princípios-chave e melhores práticas para ajudar você a otimizar suas regras, reduzir computação desnecessária e evitar armadilhas comuns. Ele incorpora insights de especialistas do setor, incluindo Victor M. Alvarez, WXS, e contribuições da comunidade YARA.
Esta seção fornece um resumo conciso das melhores práticas de desempenho do YARA. Para explicações detalhadas e exemplos, consulte o guia completo abaixo.
"Pense no YARA como um processo de duas etapas: primeiro, buscar todos os padrões listados nas strings e, segundo, avaliar as condições. Você não pode usar condições bem formadas para compensar strings mal escolhidas."
— Wesley Shields
O YARA segue quatro etapas principais ao varrer um arquivo:
O YARA busca strings primeiro, tornando a seleção de strings o fator mais importante para a eficiência da regra.
✅ Melhores Práticas para Strings:
\x00\x00\x00\x00 aparece com frequência demais.nocase com cuidado – Ele gera exponencialmente mais variações de busca.O YARA avalia condições sequencialmente e para na primeira falha.
✅ Melhores Práticas para Condições:
filesize < X) antes de condições caras.for all i in (1..filesize) é ineficiente).@) em vez de regex para verificações de sequência.⚠ Nota: Condições com regex não usam curto-circuito e são sempre avaliadas por último.
Módulos como pe, elf ou magic precisam analisar o arquivo inteiro antes da avaliação, aumentando o tempo de varredura.
✅ Alternativas:
pe.is_pe, use uint16(0) == 0x5A4D para identificar arquivos PE.Correspondências excessivas tornam a varredura mais lenta e podem acionar erros de "too many matches".
✅ Corrigindo Correspondências Ineficientes:
.*, .+ ou {x,} sem limite superior.@herrcore criou um tutorial em vídeo útil cobrindo os tópicos discutidos neste guia de desempenho.
Introduction Into YARA - Writing Efficient YARA Rules
Para entender melhor o que e onde o desempenho do YARA pode ser otimizado, é útil compreender o processo de varredura. Ele é basicamente dividido em 4 etapas, que serão explicadas de forma bem simplificada usando esta regra de exemplo:
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
}
Esta etapa ocorre antes da varredura em si. O YARA procura os chamados atoms nas strings de busca para alimentar o autômato Aho-Corasick. Os detalhes são explicados no capítulo átomo, mas por enquanto basta saber que eles têm no máximo 4 bytes e o YARA os escolhe de forma bastante inteligente para evitar muitas correspondências. Em nosso exemplo, o YARA pode escolher os seguintes 4 átomos:
<?phGETPOSTsser (de assert)Aqui a varredura começa. As etapas 2.-4. serão executadas em todos os arquivos. O YARA procurará em cada arquivo os 4 átomos definidos acima usando uma árvore de prefixos chamada autômato Aho-Corasick. Quaisquer correspondências são repassadas ao mecanismo de bytecode.
Se houver, por exemplo, uma correspondência em sser, o YARA verificará se ela foi precedida por um a e continua com um t. Se isso for verdade, ele prosseguirá com a regex [\t ]{0,100}\(. Com essa abordagem inteligente, o YARA evita percorrer os arquivos inteiros com um mecanismo de regex lento e apenas seleciona determinadas partes para examinar mais de perto.
Após toda a correspondência de padrões ser concluída, as condições são verificadas.
O YARA possui outro mecanismo de otimização para executar a verificação de math.entropy, que exige muita CPU, da nossa regra de exemplo somente se as 4 condições anteriores forem atendidas. Explicado em mais detalhes no capítulo Condições e Avaliação de Curto-Circuito
Se as condições forem atendidas, uma correspondência é relatada. A varredura continua com o próximo arquivo na etapa 2.
O YARA extrai das strings substrings curtas de até 4 bytes, chamadas de "átomos". Esses átomos podem ser extraídos de qualquer lugar dentro da string, e o YARA procura por esses átomos durante a varredura do arquivo; se encontrar um dos átomos, ele verifica se a string realmente corresponde.
Por exemplo, considere estas strings:
/abc.*cde/
=> os átomos possíveis são abc e cde, e qualquer um deles pode ser usado. O átomo abc é atualmente preferido porque ambos têm a mesma qualidade e ele é o primeiro dos dois.
/(one|two)three/
=> os átomos possíveis são one, two, thre e hree; podemos procurar por thre (ou hree) isoladamente, ou por one e two juntos. O átomo thre é preferido porque levará a menos correspondências potenciais do que one e two (que são mais curtos) e não contém e duplo (quanto mais única a letra, melhor).
O YARA faz o melhor esforço para selecionar os melhores átomos de cada string, por exemplo:
{ 00 00 00 00 [1-4] 01 02 03 04 }
=> aqui o YARA usa o átomo 01 02 03 04, porque 00 00 00 00 é comum demais
{ 01 02 [1-4] 01 02 03 04 }
=> 01 02 03 04 é preferido em relação a 01 02 porque é mais longo
Portanto, o ponto importante é que as strings devem conter bons átomos. Estas são strings ruins porque contêm átomos muito curtos ou comuns demais:
{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)/
As piores strings são aquelas que não contêm nenhum átomo, como:
/\w.*\d/
/[0-9]+\n/
Essa expressão regular não contém nenhuma substring fixa que possa ser usada como átomo, portanto ela deve ser avaliada em cada offset do arquivo para verificar se corresponde ali.
Outra boa recomendação é evitar loops com muitas iterações, especialmente se a instrução dentro do loop for complexa demais, por exemplo:
strings:
$a = {00 00}
condition:
for all i in (1..#a) : (@a[i] < 10000)
Esta regra tem dois problemas. O primeiro é que a string $a é comum demais; o segundo é que, por $a ser comum demais, #a pode ser muito alto e a condição pode ser avaliada milhares de vezes.
Esta outra condição também é ineficiente porque o número de iterações depende do tamanho do arquivo, que também pode ser muito alto:
for all i in (1..filesize) : ($a at i)
Evite usar o "módulo magic", que não está disponível na plataforma Windows. O uso do módulo "magic" torna a varredura mais lenta, mas fornece correspondências exatas.
Definição personalizada de cabeçalho mágico de GIF:
rule gif_1 {
condition:
(uint32be(0) == 0x47494638 and uint16be(4) == 0x3961) or
(uint32be(0) == 0x47494638 and uint16be(4) == 0x3761)
}
Usando o módulo "magic":
import "magic"
rule gif_2 {
condition:
magic.mime_type() == "image/gif"
}
Evite definir strings muito curtas. Qualquer string com menos de 4 bytes provavelmente aparecerá em muitos arquivos OU como conteúdo uniforme em um arquivo submetido a XOR.
Algumas strings são longas o suficiente, mas não devem ser usadas por um motivo diferente: uniformidade. Estes são alguns exemplos de strings que não devem ser usadas, pois podem causar muitas correspondências em arquivos.
$s1 = "22222222222222222222222222222222222222222222222222222222222222"
$s2 = "\x00\x20\x00\x20\x00\x20\x00\x20\x00\x20\x00\x20\x00\x20" // wide formatted spaces
A mensagem de erro seria assim:
error scanning yara-killer.dat: string "$mz" in rule "shitty_mz" caused too many matches
Tente definir strings da forma mais restrita possível. Evite o atributo "nocase" se possível, pois muitos átomos serão gerados e pesquisados (maior uso de memória, mais iterações). Lembre-se de que, na ausência de modificadores, "ascii" é assumido por padrão. As combinações possíveis são:
BAIXO - apenas um átomo é gerado
$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 - Todas as combinações de letras maiúsculas e minúsculas para os 4 bytes escolhidos pelo YARA serão geradas como átomos
$s5 = "cmd.exe" nocase (all different cases, e.g. "Cmd.", "cMd.", "cmD." ..)
Se você quiser corresponder a comandos de script, verifique se a linguagem é realmente insensível a maiúsculas/minúsculas (ex.: php, batch do Windows) antes de usar nocase. Se você precisar apenas de variações de caixa para uma ou duas letras, é melhor usar uma regex, ex.:
$re = /[Pp]assword/
Tenha cuidado ao trabalhar com alternâncias como:
$re = /(a|b)cde/
$hex = {C7 C3 00 (31 | 33)}
Essas strings geram átomos curtos que podem tornar a varredura mais lenta. Nos casos em que há um pequeno número de variantes, é recomendável escrever as strings separadamente:
$re1 = /acde/
$re2 = /bcde/
$hex1 = {C7 C3 00 31}
$hex2 = {C7 C3 00 33}
Use expressões regulares somente quando necessário. A avaliação de expressões regulares é inerentemente mais lenta do que a correspondência simples de strings e consome uma quantidade significativa de memória. Não as use se strings hexadecimais com saltos e curingas puderem resolver o problema.
Se você precisar usar expressões regulares, evite quantificadores gananciosos .* e até mesmo quantificadores relutantes .*?. Em vez disso, use números exatos como .{1,30} ou até .{1,3000}. Além disso, não se esqueça do limite superior (evite, por exemplo, .{2,}).
Ao usar quantificadores, duas situações podem ocorrer:
Se o início da expressão regular estiver ancorado em uma posição e apenas o sufixo puder variar, o YARA corresponderá à correspondência mais longa possível. Em casos como .* e .+ ou .{2,}, isso pode gerar strings grandes e problemas de lentidão na varredura.
Se houver mais possíveis inícios para a expressão regular, o YARA corresponderá a todos eles.
$re1 = /Tom.{0,2}/ // will find Tomxx in "Tomxx"
$re2 = /.{0,2}Tom/ // will find Tom, xTom, xxTom in "xxTom"
O número de correspondências mais curtas pode facilmente ultrapassar o limite e criar o erro "too many matches".
O exemplo a seguir é a expressão regular para um endereço de e-mail. Ao usar [-a-z0-9._%+] com quantificadores, o YARA corresponderá a um mesmo endereço várias vezes, o que não é ideal. Nesse caso, é recomendável encontrar um subconjunto razoavelmente pequeno de endereços que forneça informações suficientes para a análise.
USE
/[-a-z0-9._%+]@[-a-z0-9.]{2,10}\.[a-z]{2,4}/
OR
/@[-a-z0-9.]{2,10}\.[a-z]{2,4}/
AVOID
/[-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 você quiser garantir que, por exemplo, exec seja seguido por /bin/sh, você pode usar os offsets fornecidos pelo símbolo @. Esta seria a versão lenta com regex:
$ = /exec.*\/bin\/sh/
Esta é a forma mais rápida usando offsets:
strings:
$exec = "exec"
$sh = "/bin/sh"
conditions:
$exec and $sh and
@exec < @sh
Tente também incluir sequências longas de strings que possam servir como âncoras no processo de correspondência. Novamente, quanto mais longo, melhor.
RUIM
$s1 = /http:\/\/[.]*\.hta/ // greedy [.]*
MELHOR
$s1 = /http:\/\/[a-z0-9\.\/]{3,70}\.hta/ // better, with an the upper bound
MELHOR AINDA
$s1 = /mshta\.exe http:\/\/[a-z0-9\.\/]{3,70}\.hta/
Os erros de "too many matches" são causados por strings muito genéricas que aparecem com frequência demais na entrada, ou pelo YARA correspondendo a uma mesma ocorrência várias vezes.
A lentidão na varredura é causada por strings que geram átomos curtos demais, ou nenhum átomo. Como resultado, o YARA usa um algoritmo ingênuo de correspondência de padrões, o que causa a lentidão.
Ambos os problemas podem, em alguns casos, ser corrigidos com estas etapas:
.* e .+, .*?x{14,}Observe que, no próximo capítulo Condições e Avaliação de Curto-Circuito, algumas dicas para condições são mencionadas. No entanto, as alterações nelas não resolverão os erros de "too many matches" e lentidão na varredura.
Tente escrever declarações de condição nas quais os elementos com maior probabilidade de serem "False" sejam colocados primeiro. A condição é avaliada da esquerda para a direita. Quanto mais cedo o mecanismo identificar que uma regra não foi satisfeita, mais cedo ele poderá pular a regra atual e avaliar a próxima. A melhoria de velocidade causada por essa forma de ordenar as declarações de condição depende da diferença nos ciclos de CPU necessários para processar cada uma delas. Se todas as declarações tiverem custo mais ou menos igual, reordená-las não causa melhoria perceptível. Se uma das declarações puder ser processada muito rapidamente, é recomendável colocá-la primeiro, a fim de evitar a avaliação da declaração cara nos casos em que a primeira declaração for FALSE.
Alterar a ordem na declaração a seguir não causa melhoria significativa:
$string1 and $string2 and uint16(0) == 0x5A4D
No entanto, se o tempo de execução das declarações for muito diferente, reordená-las para acionar o curto-circuito melhorará significativamente a velocidade da varredura:
LENTO
// EXPENSIVE and CHEAP
math.entropy(0, filesize) > 7.0 and uint16(0) == 0x5A4D
RÁPIDO
// CHEAP and EXPENSIVE
uint16(0) == 0x5A4D and math.entropy(0, filesize) > 7.0
A avaliação de curto-circuito foi introduzida para ajudar a otimizar sentenças caras, particularmente sentenças "for". Algumas pessoas usavam condições como a do exemplo a seguir:
strings:
$mz = "MZ"
...
condition:
$mz at 0 and for all i in (1..filesize) : ( whatever )
Como filesize pode ser um número muito grande, "whatever" pode ser executado muitas vezes, tornando a execução mais lenta. Agora, com a avaliação de curto-circuito, a sentença "for" será executada somente se a primeira parte da condição for atendida; portanto, esta regra será lenta apenas para arquivos MZ. Uma melhoria adicional poderia ser:
$mz at 0 and filesize < 100KB and for all i in (1..filesize) : ( whatever )
Dessa forma, um limite superior para o número de iterações é definido.
A partir da versão 3.10, os loops de faixa de inteiros também foram otimizados:
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.
Infelizmente, isso não funciona com expressões regulares porque todas elas são inicialmente alimentadas no mecanismo de correspondência de strings. O exemplo a seguir tornará a busca mais lenta para qualquer arquivo, e não apenas para aqueles com tamanho menor que 200 bytes:
strings:
$expensive_regex = /\$[a-z0-9_]+\(/ nocase
conditions:
filesize < 200 and
$expensive_regex
Essa avaliação de "curto-circuito" é aplicada desde a versão 3.4 do YARA.
Quaisquer dados na seção de metadados são lidos na RAM pelo YARA. (Você pode testar isso facilmente inserindo 100.000 hashes em uma regra e verificando o uso de RAM da varredura do YARA antes e depois.) É claro que você não quer remover permanentemente os metadados das regras, mas se estiver com pouca RAM, você pode remover algumas partes desnecessárias deles em seu fluxo de trabalho imediatamente antes da varredura.