
Руководство по написанию быстрых и эффективных по памяти YARA-правил
Написание эффективных YARA-правил необходимо для поддержания быстрого и точного сканирования. Это руководство содержит ключевые принципы и лучшие практики, которые помогут вам оптимизировать правила, снизить ненужные вычисления и избежать типичных ошибок. Оно включает наработки экспертов индустрии, в том числе Виктора М. Альвареса (Victor M. Alvarez), WXS, а также вклад сообщества YARA.
В этом разделе приведено краткое резюме лучших практик производительности YARA. За подробными объяснениями и примерами обращайтесь к полному руководству ниже.
"Представляйте YARA как двухэтапный процесс: сначала поиск всех шаблонов, перечисленных в строках, а затем оценка условий. Нельзя компенсировать плохо подобранные строки хорошо составленными условиями."
— Уэсли Шилдс (Wesley Shields)
При сканировании файла YARA выполняет четыре основных шага:
YARA сначала ищет строки, поэтому выбор строк является самым важным фактором эффективности правила.
✅ Лучшие практики для строк:
\x00\x00\x00\x00 встречается слишком часто.nocase с осторожностью — это порождает экспоненциально больше вариантов поиска.YARA вычисляет условия последовательно и останавливается при первой неудаче.
✅ Лучшие практики для условий:
filesize < X) перед дорогостоящими условиями.for all i in (1..filesize) неэффективно).@) вместо регулярных выражений для проверки последовательностей.⚠ Примечание: Условия с регулярными выражениями не поддерживают сокращённые вычисления и всегда оцениваются последними.
Такие модули, как pe, elf или magic, должны разобрать весь файл перед оценкой, что увеличивает время сканирования.
✅ Альтернативы:
pe.is_pe используйте uint16(0) == 0x5A4D для определения PE-файлов.Чрезмерное количество совпадений замедляет сканирование и может вызывать ошибки "too many matches" (слишком много совпадений).
✅ Исправление неэффективных совпадений:
.*, .+ или {x,} без верхней границы.@herrcore создал полезный видеоурок, охватывающий темы, обсуждаемые в этом руководстве по производительности.
Чтобы лучше понять, что и где можно оптимизировать в производительности YARA, полезно разобраться в процессе сканирования. Он в основном разделён на 4 шага, которые будут объяснены очень упрощённо на примере этого правила:
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
}
Этот шаг выполняется до фактического сканирования. YARA ищет так называемые атомы в строках поиска, чтобы передать их автомату Ахо — Корасик. Подробности объяснены в главе атом, но пока достаточно знать, что их максимальная длина составляет 4 байта, и YARA выбирает их довольно умно, чтобы избежать слишком большого количества совпадений. В нашем примере YARA может выбрать следующие 4 атома:
<?phGETPOSTsser (из assert)Здесь сканирование началось. Шаги 2–4 выполняются для всех файлов. YARA ищет в каждом файле 4 атома, определённые выше, с помощью префиксного дерева, называемого автоматом Ахо — Корасик. Все совпадения передаются движку байт-кода.
Если, например, есть совпадение по sser, YARA проверит, предшествовала ли ему буква a, и продолжит с t. Если это так, он перейдёт к регулярному выражению [\t ]{0,100}\(. Благодаря такому умному подходу YARA не обрабатывает медленным регулярным выражением все файлы целиком, а выбирает только определённые части для более детального изучения.
После завершения сопоставления с шаблонами проверяются условия.
В YARA есть ещё один механизм оптимизации: ресурсоёмкая проверка math.entropy из нашего примера выполняется только в том случае, если выполнены 4 предыдущих условия. Подробнее это объясняется в главе Условия и сокращённые вычисления
Если условия выполнены, сообщается о совпадении. Сканирование продолжается со следующим файлом на шаге 2.
YARA извлекает из строк короткие подстроки длиной до 4 байт, которые называются "атомами". Эти атомы могут быть извлечены из любого места строки, и YARA ищет эти атомы при сканировании файла; если он находит один из атомов, то проверяет, действительно ли строка совпадает.
Например, рассмотрим такие строки:
/abc.*cde/
=> возможные атомы — abc и cde, можно использовать любой из них. В настоящее время предпочтителен атом abc, поскольку они имеют одинаковое качество, и это первый из двух.
/(one|two)three/
=> возможные атомы — one, two, thre и hree; можно искать только thre (или hree) либо оба one и two. Атом thre предпочтительнее, потому что он приведёт к меньшему числу потенциальных совпадений, чем one и two (они короче), и не содержит двойной e (чем уникальнее буква, тем лучше).
YARA прикладывает все усилия, чтобы выбрать лучшие атомы из каждой строки, например:
{ 00 00 00 00 [1-4] 01 02 03 04 }
=> здесь YARA использует атом 01 02 03 04, потому что 00 00 00 00 слишком распространён
{ 01 02 [1-4] 01 02 03 04 }
=> 01 02 03 04 предпочтительнее, чем 01 02, потому что он длиннее
Итак, важный момент: строки должны содержать хорошие атомы. Вот плохие строки, потому что они содержат либо слишком короткие, либо слишком распространённые атомы:
{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)/
Худшие строки — те, которые вообще не содержат атомов, например:
/\w.*\d/
/[0-9]+\n/
Эти регулярные выражения не содержат ни одной фиксированной подстроки, которую можно было бы использовать в качестве атома, поэтому их необходимо проверять на каждом смещении файла, чтобы увидеть, есть ли там совпадение.
Ещё одна хорошая рекомендация — избегать циклов со слишком большим количеством итераций, особенно если оператор внутри цикла слишком сложный, например:
strings:
$a = {00 00}
condition:
for all i in (1..#a) : (@a[i] < 10000)
У этого правила две проблемы. Первая — строка $a слишком распространена, вторая — из-за того, что $a слишком распространена, #a может быть слишком большим, и условие может вычисляться тысячи раз.
Следующее условие также неэффективно, потому что количество итераций зависит от размера файла, который также может быть очень большим:
for all i in (1..filesize) : ($a at i)
Избегайте использования модуля "magic", который недоступен на платформе Windows. Использование модуля "magic" замедляет сканирование, но обеспечивает точные совпадения.
Пользовательское определение заголовка GIF:
rule gif_1 {
condition:
(uint32be(0) == 0x47494638 and uint16be(4) == 0x3961) or
(uint32be(0) == 0x47494638 and uint16be(4) == 0x3761)
}
С использованием модуля "magic":
import "magic"
rule gif_2 {
condition:
magic.mime_type() == "image/gif"
}
Избегайте определения слишком коротких строк. Любая строка короче 4 байт, вероятно, встретится во множестве файлов ИЛИ как однородное содержимое в XOR-файле.
Некоторые строки достаточно длинны, но не должны использоваться по другой причине — однородности. Вот несколько примеров строк, которые не следует использовать, так как они могут вызвать слишком много совпадений в файлах.
$s1 = "22222222222222222222222222222222222222222222222222222222222222"
$s2 = "\x00\x20\x00\x20\x00\x20\x00\x20\x00\x20\x00\x20\x00\x20" // широкие пробелы в UTF-16
Сообщение об ошибке будет выглядеть так:
error scanning yara-killer.dat: string "$mz" in rule "shitty_mz" caused too many matches
Старайтесь описывать определения строк как можно более узко. По возможности избегайте атрибута "nocase", потому что будет сгенерировано и найдено множество атомов (более высокое использование памяти, больше итераций). Помните: при отсутствии модификаторов по умолчанию предполагается "ascii". Возможные комбинации:
НИЗКО — генерируется только один атом
$s1 = "cmd.exe" // (только ascii)
$s2 = "cmd.exe" ascii // (только ascii, то же, что и $s1)
$s3 = "cmd.exe" wide // (только UTF-16)
$s4 = "cmd.exe" ascii wide // (и ascii, и UTF-16) будет сгенерировано два атома
$s5 = { 63 6d 64 2e 65 78 65 } // ascii-коды символов в hex
ВЫСОКО — все комбинации верхнего и нижнего регистра для 4 байт, выбранных YARA, будут сгенерированы как атомы
$s5 = "cmd.exe" nocase (все различные варианты регистра, например "Cmd.", "cMd.", "cmD." ..)
Если вы хотите сопоставлять команды сценариев, прежде чем использовать nocase, проверьте, является ли язык вообще регистронезависимым (например, PHP, пакетные файлы Windows). Если вам нужен другой регистр только для одной или двух букв, лучше использовать регулярное выражение, например:
$re = /[Pp]assword/
Будьте осторожны с альтернациями, такими как:
$re = /(a|b)cde/
$hex = {C7 C3 00 (31 | 33)}
Эти строки порождают короткие атомы, которые могут замедлить сканирование. В случаях, когда вариантов немного, рекомендуется записывать строки отдельно:
$re1 = /acde/
$re2 = /bcde/
$hex1 = {C7 C3 00 31}
$hex2 = {C7 C3 00 33}
Используйте регулярные выражения только при необходимости. Вычисление регулярных выражений по своей сути медленнее, чем простое сопоставление строк, и потребляет значительный объём памяти. Не используйте их, если задачу можно решить с помощью hex-строк с переходами и подстановочными знаками.
Если вам приходится использовать регулярные выражения, избегайте жадных .* и даже «ленивых» квантификаторов .*?. Вместо этого используйте точные числа, например .{1,30} или даже .{1,3000}. Также не забывайте про верхнюю границу (избегайте, например, .{2,}).
При использовании квантификаторов возможны две ситуации:
Если начало регулярного выражения привязано к одной позиции и может изменяться только суффикс, YARA найдёт максимально возможное совпадение. В случаях с .* и .+ или .{2,} это может привести к очень длинным строкам и замедлению сканирования.
Если у регулярного выражения есть несколько возможных начал, YARA найдёт все из них.
$re1 = /Tom.{0,2}/ // найдёт Tomxx в "Tomxx"
$re2 = /.{0,2}Tom/ // найдёт Tom, xTom, xxTom в "xxTom"
Количество более коротких совпадений может легко превысить лимит и вызвать ошибку "too many matches" (слишком много совпадений).
Следующий пример — регулярное выражение для адреса электронной почты. При использовании [-a-z0-9._%+] с квантификаторами YARA будет сопоставлять один адрес несколько раз, что не идеально. В этом случае рекомендуется найти достаточно небольшое подмножество адресов, дающее достаточно информации для анализа.
ИСПОЛЬЗУЙТЕ
/[-a-z0-9._%+]@[-a-z0-9.]{2,10}\.[a-z]{2,4}/
ИЛИ
/@[-a-z0-9.]{2,10}\.[a-z]{2,4}/
ИЗБЕГАЙТЕ
/[-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}/
Если вы хотите убедиться, что, например, за exec следует /bin/sh, можно использовать смещения, предоставляемые символом @. Это была бы медленная версия с регулярным выражением:
$ = /exec.*\/bin\/sh/
Вот более быстрый способ со смещениями:
strings:
$exec = "exec"
$sh = "/bin/sh"
conditions:
$exec and $sh and
@exec < @sh
Также старайтесь включать длинные последовательности строк, которые могут служить якорями в процессе сопоставления. Опять же, чем длиннее, тем лучше.
ПЛОХО
$s1 = /http:\/\/[.]*\.hta/ // жадный [.]*
ЛУЧШЕ
$s1 = /http:\/\/[a-z0-9\.\/]{3,70}\.hta/ // лучше, с верхней границей
НАИЛУЧШЕ
$s1 = /mshta\.exe http:\/\/[a-z0-9\.\/]{3,70}\.hta/
Ошибки "too many matches" (слишком много совпадений) вызваны слишком общими строками, которые слишком часто встречаются во входных данных, либо YARA сопоставляет один экземпляр несколько раз.
Замедление сканирования вызвано строками, которые порождают слишком короткие атомы или не порождают их вовсе. В результате YARA использует наивный алгоритм сопоставления с шаблоном, что и вызывает замедление.
Обе эти проблемы в некоторых случаях можно исправить следующими шагами:
.* и .+, .*?x{14,}Обратите внимание: в следующей главе Условия и сокращённые вычисления приведено несколько советов по условиям. Однако их изменение не решит проблемы "too many matches" и замедления сканирования.
Старайтесь записывать условия так, чтобы элементы, которые с наибольшей вероятностью дадут "False", располагались первыми. Условие вычисляется слева направо. Чем раньше движок определит, что правило не выполнено, тем быстрее он сможет пропустить текущее правило и перейти к следующему. Ускорение, вызванное таким порядком условий, зависит от разницы в необходимых тактах ЦП для обработки каждого из операторов. Если все операторы примерно одинаково затратны, изменение порядка не даст заметного улучшения. Если один из операторов может быть обработан очень быстро, рекомендуется поставить его первым, чтобы пропустить дорогостоящую оценку в случаях, когда первый оператор равен FALSE.
Изменение порядка в следующем выражении не даёт значительного улучшения:
$string1 and $string2 and uint16(0) == 0x5A4D
Однако если время выполнения операторов сильно различается, изменение порядка для срабатывания сокращённых вычислений значительно ускорит сканирование:
МЕДЛЕННО
// ДОРОГО и ДЁШЕВО
math.entropy(0, filesize) > 7.0 and uint16(0) == 0x5A4D
БЫСТРО
// ДЁШЕВО и ДОРОГО
uint16(0) == 0x5A4D and math.entropy(0, filesize) > 7.0
Сокращённые вычисления были введены для оптимизации дорогостоящих конструкций, особенно циклов "for". Некоторые использовали условия, подобные примеру ниже:
strings:
$mz = "MZ"
...
condition:
$mz at 0 and for all i in (1..filesize) : ( whatever )
Поскольку filesize может быть очень большим числом, "whatever" может выполняться очень много раз, замедляя выполнение. Теперь, благодаря сокращённым вычислениям, цикл "for" будет выполняться только в том случае, если первая часть условия выполнена, поэтому это правило будет медленным только для MZ-файлов. Дополнительным улучшением может быть:
$mz at 0 and filesize < 100KB and for all i in (1..filesize) : ( whatever )
Таким образом устанавливается верхняя граница количества итераций.
Начиная с версии 3.10, циклы по целочисленным диапазонам также были оптимизированы:
for all i in (0..100): (false)
for any i in (0..100): (true)
Оба этих цикла прекратят итерации после первого прохода.
К сожалению, это не работает с регулярными выражениями, потому что все они изначально передаются в движок сопоставления строк. Следующий пример замедлит поиск для любого файла, а не только для тех, чей размер меньше 200 байт:
strings:
$expensive_regex = /\$[a-z0-9_]+\(/ nocase
conditions:
filesize < 200 and
$expensive_regex
Эта "сокращённая" оценка применяется с версии YARA 3.4.
Любые данные в секции метаданных считываются YARA в оперативную память. (Это легко проверить, вставив 100 000 хешей в правило и сравнив использование RAM при сканировании YARA до и после.) Конечно, вы не хотите навсегда удалять метаданные из правил, но если у вас мало оперативной памяти, вы можете удалить некоторые ненужные части метаданных в своём рабочем процессе непосредственно перед сканированием.