Daily MaverickJoburg crisis cannot be solved without competent City leadershipESPNPremier League depth charts for most popular teams: Who is key?The Jerusalem PostWho will teach the next generation?PunchNigeria@66: 2027 election will test our democratic maturity – JonathanBollywood HungamaDisha Patani completes a decade in Bollywood as MS Dhoni: The Untold Story turns 10: “Beyond grateful ”Il Fatto Quotidiano“Il lusso? Se c’è non me lo nego ma non lo cerco. Non ho lo yatch di 40 metri o la Lamborghini. Il mio aspetto? C’è il fascino sottile della decomposizione delle carni”: Paolo Bonolis si raccontaسكاي نيوز عربيةاستطلاع يفجر مفاجأة بشأن رونالدو.. ماذا يريد مشجعو البرتغال؟RTBF InfoDes feux et dégradations lors d'un mouvement spontané d'élèves devant plusieurs écoles de LiègeBBC News BrasilGoogle remove mais de 40 canais com falsos médicos de IA após reportagens da BBC News BrasilEgypt IndependentEgypt thwarts Israel’s plans to build Suez Canal alternative: Deputy Prime MinisterColliderAlmost 30 Years Ago, 'Buffy the Vampire Slayer' Did 'Obsession' in This Action-Packed EpisodeRMF24Korea prostuje słowa Trumpa o inwestycjach za miliardy
The Daily Newsstand · Free, Always
Thursday, October 1, 2026

[Перевод] Оптимизация правил YARA: руководство по ускорению и точности сканирования

Translate

Грамотно написанные YARA-правила обеспечивают высокую скорость и точность сканирования. Однако при неправильном подходе этот полезный инструмент «легким движением руки» превращается в пожирателя CPU, особенно когда сигнатуры исчисляются десятками, а количество файлов для анализа — миллионами.

В статье приведем полезные лайфхаки, которые помогут оптимизировать правила, сократить избыточные вычисления и избежать распространенных ошибок. Для подготовки данного гайда мы использовали материалы, размещенные в открытом доступе на гитхабе. Рекомендации статьи применимы ко всем версиям YARA, начиная с 3.7 и выше. 

Всё еще пишете YARA-правила «на глазок»? Тогда мы идем к вам…

Анатомия процесса сканирования YARA

Прежде чем переходить к оптимизации YARA-правил, не помешает пару слов сказать про устройство самого процесса сканирования. Он состоит из четырех основных этапов. Для наглядности рассмотрим их в упрощенном виде на примере следующего правила:

import "math"

rule example_php_webshell_rule

{

    meta:

        description = "Пример правила для обнаружения PHP-веб-шелла"

        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

Итак, пройдемся по каждому из этапов.

Этап 1.  Компиляция правил (подготовка)

Этот статический этап выполняется один раз при загрузке правил, до начала сканирования. YARA извлекает из строковых литералов и регулярных выражений так называемые атомы (atoms) — короткие подстроки длиной до 4 байт, которые будут служить «якорями» для быстрого поиска. Затем атомы передаются в автомат Ахо-Корасик, который будет построен на их основе. Детали выбора атомов опишем далее. Пока же отметим, что здесь мы не работаем с файлами — только готовим «карту» для будущего поиска. 

В нашем случае (см. пример выше) YARA может выбрать следующие четыре атома:

  • <?ph

  • GET

  • POST

  • sser (из последовательности assert)

Этап 2. Поиск по алгоритму Ахо-Корасик (предварительное сканирование)

Данный этап — динамический и выполняется для каждого файла отдельно. YARA «прогоняет» содержимое файла через заранее построенный автомат Ахо-Корасик, выявляя только атомы — короткие фрагменты, а не полные строки или регулярные выражения. Такая задача выполняется на уровне простого сравнения байтов. Найденные совпадения передаются движку байт-кода для дальнейшей проверки. В отличие от первого этапа, здесь мы уже взаимодействуем с данными, но лишь на поверхностном уровне — без анализа контекста.

Этап 3. Работа движка байт-кода (верификация совпадений)

На этом этапе YARA берет найденные атомы и проверяет полное совпадение с исходными строками и регулярными выражениями. Например, если движок находит атом sser, то проверяется, предшествует ли ему символ a и следует ли за ним t — так восстанавливается полная строка assert. Если это условие выполняется, продолжается проверка по регулярному выражению [\t ]{0,100}\\(. Такой подход ускоряет обработку всего файла движком регулярных выражений: анализируются лишь те фрагменты, где было найдено предварительное совпадение по атому.

Этап 4. Проверка условий (финальная логика)

Это исполнение дополнительной логики, описанной в блоке condition. Здесь работает еще один механизм оптимизации: ресурсоемкая проверка math.entropy из нашего примера запустится, только если истинны четыре предыдущих условия. Впрочем, об этом расскажем подробнее немного позже. При выполнении условий YARA сообщает о совпадении и переходит к следующему файлу.

Что же, с базовыми понятиями и этапностью сканирования в общих чертах разобрались. Теперь можно переходить к обещанным лайфхакам по повышению эффективности применения YARA-правил. 

Золотые правила выбора строк

«Работу YARA можно разбить на два шага: сначала поиск строк, потом проверка условий. Если строки подобраны неудачно, их не спасут даже идеальные условия.

Ниже — семь рекомендаций по работе со строками, которые помогут снизить риски падения производительности.

Избегайте коротких строк (менее 4 байт). Короткие строки приводят к множеству ложноположительных срабатываний. Любая такая строка с высокой вероятностью встретится в массе файлов. Или же она окажется однородным содержимом в файле, обработанном операцией XOR.

Используйте уникальные 4-байтные атомы. На них основан механизм быстрого сканирования YARA.

Минимизируйте символы подстановки (wildcards) в HEX-строках. Оставляйте хотя бы один длинный непрерывный сегмент данных.

Применяйте регулярные выражения обдуманно. При необходимости добавляйте фиксированный 4-байтный «якорь» для повышения производительности.

Избегайте паттернов из повторяющихся байтов (например, \x00\x00\x00\x00), так как они встречаются слишком часто.

Некоторые строки не стоит использовать из-за их однотипности (даже если они обладают достаточной длиной). Ниже приведены примеры строк, от которых стоит отказаться, поскольку они могут вызывать слишком большое количество совпадений в файлах.

$s1 = "22222222222222222222222222222222222222222222222222222222222222"

$s2 = "\x00\x20\x00\x20\x00\x20\x00\x20\x00\x20\x00\x20\x00\x20"  // пробелы в кодировке wide

Сообщение об ошибке будет выглядеть так:

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

Высокая нагрузка (генерируются атомы для всех комбинаций регистра):

$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}

Лайфхаки по работе с условиями

Если строки — это фундамент сканирования, то условия можно сравнить с несущими стенами. В конце концов, они тоже во многом определяют, выдержит ли система нагрузку при проверке YARA-правилами. 

Несколько полезных лайфхаков по работе с условиями.

Правило 1. Сначала выполняйте быстрые проверки

YARA проверяет условия слева направо, останавливается на первом же несовпадении (результате, возвращающем 'false') и переходит к следующему правилу. Выигрыш в скорости от такого порядка зависит от разницы в ресурсоемкости вычисления каждого из выражений. Если выражения равнозначны по затратам, их порядок не важен. Если же одно из них вычисляется быстрее, его следует ставить первым в блоке conditions, чтобы избежать проверки более ресурсоемких условий. Например, перестановка в следующем условии не даст значительного ускорения, так как все проверки относительно быстрые:

 $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

Правило 2. Избегайте циклов по большим объемам данных 

Механизм вычисления по короткой схеме был добавлен в YARA для оптимизации ресурсоемких условий, в первую очередь содержащих цикл for. Ранее некоторые специалисты писали правила со следующими условиями:

strings:

        $mz = "MZ"

        ...

condition:

        $mz at 0 and for all i in (1..filesize) : (некое_условие)

Поскольку значение filesize может быть очень большим, тело цикла (некое_условие) выполнялось множество раз, что сильно замедляло процесс сканирования. Теперь, с вычислением по короткой схеме, цикл for выполняется, только если первая часть условия истинна. Правило замедляется лишь на MZ-файлах. Также улучшить производительность можно ограничением размера файла:

$mz at 0 and filesize < 100KB and for all i in (1..filesize) : (некое_условие)

Таким образом, задается верхний предел для количества итераций цикла.

 В YARA 3.10 также оптимизировали циклы по диапазону целых чисел:

 for all i in (0..100): (false)

 for any i in (0..100): (true)

Оба цикла прервутся после первой же итерации.

Правило 3. Для проверки данных по известному смещению используйте указатель положения (@) вместо регулярных выражений

Условия с регулярными выражениями всегда проверяются в последнюю очередь и не используют вычисления по короткой схеме, так как они заранее передаются в движок поиска строк. Поэтому пример ниже замедлит сканирование всех файлов, а не только тех, что меньше 200 байт:

strings:

  $expensive_regex = /\$[a-z0-9_]+\(/ nocase

conditions:

  filesize < 200 and

  $expensive_regex

Вычисление по короткой схеме доступно с версии YARA 3.4.

Модули и производительность сканирования

Модули pe, elf, magic и другие действительно бывают полезны при сканировании и глубоком анализе структуры файла. Правда, они должны полностью проанализировать (распарсить) файл перед началом проверки, что замедляет сканирование.

Поэтому применять модули важно обдуманно, иначе проверка превращается в стрельбу из «Джавелина» по воробьям. Скажем, если для логики правила не требуется глубокий анализ структуры файла, то модули лучше не использовать.

Возьмем для примера модуль [magic]. Да, этот модуль позволяет получать точные совпадения по типам файлов на основе их сигнатур, однако замедляет сканирование и недоступен на платформе Windows.

Пример определения заголовка 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"

}

Избыточные совпадения и ошибка too many matches 

Еще одна распространенная причина просадки производительности и затягивания сканирования — слишком большое количество совпадений строк. К тому же это может привести к ошибке too many matches.

Вот как можно исправить неэффективные правила и избежать ошибки too many matches:

  • в регулярных выражениях избегайте квантификаторов .*, .+ или {x,} без верхней границы.

  • сократите количество символов подстановки (wildcards) в HEX-строках.

  • по возможности разделяйте | (операторы чередования) на отдельные строки.

К слову, в OpenAnalysis Inc (@herrcore) есть наглядный видеоурок, как написать эффективные YARA-правила.

Поиск неэффективных строк 

Ошибка «слишком много совпадений» (too many matches) возникает, когда строки в правиле чересчур общие и часто встречаются в проверяемых данных. Другая возможная причина — YARA находит множество пересекающихся совпадений для одного фрагмента.

Замедление сканирования вызывают строки, которые генерируют слишком короткие атомы или не создают их вовсе. В результате YARA вынуждена использовать медленный и наивный алгоритм поиска по шаблону, что и приводит к падению производительности. 

В некоторых случаях обе эти проблемы решаются следующими действиями:

  • проверкой наличия «жадных» и «ленивых» квантификаторов ( .*, .+, .*?) или квантификаторов без верхней границы (например, x{14,});

  • проверкой наличия слишком больших диапазонов (например, x{1,300000}), или больших «прыжков» (jumps) в шестнадцатеричных строках;

  • анализом использования подстановочных знаков (wildcards). Например, можно ли заменить их более точными символами или разбить строку на две, исключив подстановочный знак;

  • включением модификатора для поиска целых слов (например, fullword, \b);

  • использованием оператора чередований. Например, проверяем, можно ли разбить строку на две или более отдельные строки?

Эффективные и неэффективные атомы

Повторимся, что YARA выделяет из строк короткие подстроки длиной до 4 байт — атомы. Они могут быть взяты из любого участка строки. При сканировании файла YARA ищет именно эти атомы, а при обнаружении хотя бы одного — выполняет полную проверку на соответствие всей строки.

Рассмотрим, как это работает на примерах.

Скажем, в строке /abc.*cde/ возможные атомы — abc и cde.YARA может использовать любой из них, но в данном случае выберет abc, поскольку при одинаковом «качестве» он стоит первым.

Возможными атомами для строки /(one|two)three/ будут one, two, thre и hree. В теории YARA может искать либо thre (или hree) отдельно, либо one и two вместе. Однако выбор почти гарантированно падет на thre, так как он выдаст меньше потенциальных совпадений, чем one и two (они короче). К тому же thre не содержит повторяющуюся букву e (в отличие от hree), что делает его комбинацию символов более уникальной.

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/

В этих регулярных выражениях нет ни одной фиксированной подстроки, которую можно было бы использовать в качестве атома. Поэтому YARA придется проверять такое выражение по каждому смещению в файле, что крайне ресурсозатратно.

Влияние количества итераций в цикле на производительность

Избегайте слишком большого числа итераций в цикле. Особенно это актуально, если тело цикла содержит сложные инструкции for. И снова рассмотрим правило на примере:

strings:

        $a = {00 00}

condition:

        for all i in (1..#a) : (@a[i] < 10000)

У этого правила две проблемы. Во-первых, строка $a слишком распространена. Во-вторых, из-за этого счетчик ее вхождений #a может достигать огромных значений, что приводит к тысячам итераций цикла и многократной проверке условия.

Следующее условие тоже крайне неэффективно, поскольку число итераций в его цикле зависит от размера файла (filesize), который может быть чрезмерно большим:

for all i in (1..filesize) : ($a at i)

Регулярные выражения и производительность 

Как и обещали, возвращаемся к теме регулярных выражений. Их обработка по определению медленнее, чем сопоставление с обычными строками, и требует значительного объема памяти. В качестве альтернативы зачастую можно использовать hex-строки с пропусками и подстановочными символами.

Если без регулярных выражений всё же не обойтись, избегайте как жадных (.*), так и ленивых (.*?) квантификаторов. Вместо них следует использовать конструкции с точным указанием числа повторений, например, .{1,30} или даже .{1,3000}.  Также важно всегда задавать верхнюю границу диапазона (то есть избегать выражений вроде .{2,}).

При использовании квантификаторов возможны две ситуации.

Ситуация 1. Начало регулярного выражения зафиксировано, а варьируется только его окончание. Тогда YARA найдет одно, самое длинное из возможных совпадений. В случае с неограниченными квантификаторами (.*, .+, .{2,}) это может привести к захвату очень длинных строк и замедлению сканирования.

Ситуация 2. Варьируется начало выражения. В таком случае YARA найдет все возможные совпадения. 

$re1 = /Tom.{0,2}/              // в строке "Tomxx" найдет одно совпадение: "Tomxx"

 $re2 = /.{0,2}Tom/      // в строке "xxTom" найдет три совпадения: "Tom", "xTom", "xxTom"

Множественные совпадения могут вызвать ошибку too many matches.

Рассмотрим в качестве примера регулярное выражение для поиска адресов электронной почты. Применение квантификаторов (*, +, {x,y}) к части адреса перед символом @ (например, [-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), вместо регулярных выражений лучше использовать проверку смещений по файлу, доступную через символ @.

Старайтесь также включать в правила длинные строковые последовательности в качестве «якорей» для поиска совпадений. Чем они длиннее, тем лучше.

Пример

Плохой вариант:

 $s1 = /http:\/\/[.]*\.hta/    // «жадный» квантификатор [.]*

Оптимальный вариант: 

$s1 = /mshta\.exe http:\/\/[a-z0-9\.\/]{3,70}\.hta/

YARA — это инструмент, который экономит ваше время, если вы экономите его ресурсы. Надеемся, что этот гайд поможет оптимизировать сканирование, и пусть ваши сигнатуры работают на вас, а не против вас.

View the original on Хабр →

KioskNews shows a cleaned-up reading view extracted from the publisher’s page — the original always lives on their site, not ours.