Написать рабочее yara-правило несложно. Создать такое правило, которое через полгода поймет не только автор, но и любой другой аналитик в команде — задачка со звездочкой. У YARA нет строгого стандарта, как называть правило, что писать в meta и как оформлять условие. Каждый пишет как привык, и со временем база правил превращается в набор разномастных, подчас не очень логичных файлов.

                   ___________________________________________________

Привет, Хабр!

Меня зовут Максим Мотиков, я специалист по кибербезопасности в «Гарде». В нашей команде накопилось приличное количество YARA-правил. Мне прилетела задачка систематизировать их разработку. Полез разбираться, что уже есть в индустрии на эту тему, и наткнулся на YARA Style Guide. Этот гайд мне показался вполне рабочим: в нем уже собрано почти все необходимое — от правил именования до классификации строк по степени специфичности. 

Я решил не изобретать велосипед, а перевести и адаптировать это руководство под наши корпоративные нужды. Под катом делюсь переводом гайда.

P.S. Гайд не сильно углубляется в вопросы производительности, хотя соблюдение этих практик может косвенно оптимизировать работу набора правил. За более подробными стратегиями оптимизации производительности стоит обратиться к отдельному проекту автора YARA-Performance-Guidelines.

Выбор названия правила

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

Далее приведу примеры, как это может выглядеть.

Основные категории в имени правила

Категория

Расшифровка

Описание

MAL

malware

Вредоносное ПО

HKTL

hacktool

Хакерские инструменты

WEBSHELL

Веб-шеллы

EXPL

exploit

Код эксплойтов (PoC, полезные нагрузки и т. д.)

VULN

vulnerability

Уязвимости (уязвимый драйвер, уязвимая JAVA-библиотека и т. д.)

SUSP

suspicious

Аномалии и подозрительные возможности (обфусцированный код, шелл-коды, подозрительные комбинации импортов и т. д.)

PUA

possibly unwanted applications

Потенциально нежелательные приложения

Списки категорий не исчерпывающие, их можно расширить при необходимости.

Назначение и контекст

Категория

Описание

APT

Связь с APT-группировкой

CRIME

Активность киберпреступных группировок

ANOMALY

Общие подозрительные характеристики

RANSOM

Программы-вымогатели

Типы вредоносного ПО или файлов

Категория

Описание

RAT

Троян удаленного доступа

Implant

Имплант

Stealer

Стилер

Loader

Загрузчик

Crypter

Криптор

PEEXE (часто опускается)

Исполняемый PE-файл

DRV

Драйверы

Операционная система, архитектура, технология

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

Группа

Возможные значения

Операционная система

WIN (часто опускается), LNX, MacOS

Архитектура

X64 (часто опускается), X86 (часто опускается), ARM, SPARC

Технология (формат/язык)

PE (часто опускается), ELF, PS, PS1, VBS, BAT, JS, NET, GO, Rust, PHP, JSP, ASP, MalDoc, LNK, ZIP, RAR

Модификаторы

Модификатор

Описание

OBFUSC

Обфусцированные образцы

Encoded

Закодированные полезные нагрузки

Unpacked

Распакованные полезные нагрузки

InMemory

Код, обнаруживаемый только в памяти

Упаковщики или установщики

Если вредонос упакован определенным упаковщиком, это тоже можно отразить в имени. Чаще всего встречаются SFX (самораспаковывающиеся архивы), UPX, Themida и NSIS. Упаковщик в имени правила помогает аналитику сразу понять, потребуется ли предварительная распаковка для дальнейшего анализа. 

Идентификаторы субъектов и семейств угроз

Имена конкретных субъектов угроз и семейств вредоносного ПО включаются в правило как есть, без сокращений. Например, если правило детектит активность группировки Lazarus, в имя добавляется Lazarus, а не какой-то условный код. То же самое касается семейств вредоносного ПО: CobaltStrike, PlugX, QakBot и т. д. Такой подход делает имя самодокументируемым: аналитик сразу видит, с какой угрозой или группировкой связано срабатывание. 

Суффиксы для уникальности

Суффиксы снижают вероятность того, что два аналитика выберут одинаковое имя для правила.

  • MonthYear. Например, May23, Jan19

  • Number. Например, _1, _2

Комбинирование категорий

Чтобы классификация была более точной, ключевые слова лучше комбинировать (см. примеры ниже).

Имя правила 

Что означает

SUSP_APT_*

Цифровые следы в системах, скомпрометированных субъектом угрозы

MAL_CRIME_RANSOM_LNX_Rust_*

Rust-вредонос под Linux, используемый киберпреступными группами с вымогателями

WEBSHELL_APT_ASP_*

ASP-веб-шеллы, применяемые государственными акторами

Примеры полных имен правил

Имя правила

Что означает

APT_MAL_CozyBear_ELF_Loader_Apr18

Правило от апреля 2018 года для Linux-загрузчика, используемого Cozy Bear

SUSP_Anomaly_LNK_Huge_Apr22

Правило от апреля 2022 года для подозрительно больших LNK-файлов

MAL_CRIME_RANSOM_PS1_OBFUSC_Loader_May23

Правило от мая 2023 года для обфусцированного PowerShell-загрузчика в кампании с вымогателями

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

Структура правила

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

rule RULE_NAME : TAGS {

    meta:

        description = "Detects ..."

        author = "Author Name / Company / Org"

        date = "YYYY-MM-DD"

        reference = "URL / Internal Research"

        [OPTIONAL META DATA FIELDS]

    strings:

        $string1 = "value"

    condition:

        header_check

        file_size_limitation

        other_limitations

        string_combinations

        false_positive_filters

}

Далее по порядку разберу каждый элемент.

Отступы

Для повышения читаемости лучше использовать отступы. В большинстве опубликованных правил приняты 3 или 4 пробела либо табуляция.

rule MY_RULE {

   meta:

      description = "my test rule"

      author = "John Galt"

   strings:

      $s1 = "eval("

      $s2 = "WScript.Shell"

   condition:

      filesize < 10KB and all of them

}

Далее посмотрим на пример правила с «плохими» отступами.

rule MY_RULE {

meta:

description = "my test rule"

author = "John Galt"

strings:

$s1 = "eval("

$s2 = "WScript.Shell"

condition:

filesize < 10KB and all of them

}

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

rule MY_RULE {

 meta:

  description = "my test rule"

  author = "John Galt"

 strings:

  $s1 = "eval("

  $s2 = "WScript.Shell"

 condition:

  filesize < 10KB and all of them

}

Теги правил

Основные категории лучше включать прямо в имя правила, так его проще идентифицировать. Но что делать с дополнительными, менее значимыми метками? Их лучше выносить в поле метаданных tags.

rule RULE_NAME {

    meta:

        tags = "TAG1, TAG2, TAG3"

    ...

}

Такие теги могут обозначать имена субъектов угроз, семейства вредоносного ПО или типы атак.

Метаданные правила

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

rule RULE_NAME : TAGS {

    meta:

        description = "Detects ..."

        author = "Author Name / Company / Org"

        date = "YYYY-MM-DD"

        reference = "URL / Internal Research"

        score = [0-100]

        [OPTIONAL META DATA FIELDS]

    ...

}

Рассмотрим, какие поля метаданных стоит заполнять и как это лучше сделать. 

Обязательные поля

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

Поле

Значение

Рекомендации

Description

Строка, 60–400 символов

Начинать с "Detects ...". URL-адреса выносить в reference

Author

Строка

Полное имя или никнейм в Twitter. Несколько авторов через запятую в одном поле

Reference

Список строк

Прямые URL-адреса на стабильные публичные источники. Для собственных исследований указывать "Internal Research"

Date

Строка, YYYY-MM-DD

Дата создания правила, а не публикации. Для изменений есть отдельное поле modified

Необязательные поля

Эти поля не обязательны для работы правила, но дают дополнительный контекст. Особенно полезны hash (чтобы быстро проверить правило на конкретном образце) и score (чтобы расставить приоритеты при массовых срабатываниях). 

Поле

Значение

Рекомендации

Hash

Список строк

Предпочтительно SHA256. Хеш самого файла, а не архива (кроме случаев обнаружения в памяти)

Score

Число 0–100

Серьезность угрозы + специфичность правила

Modified

Строка, YYYY-MM-DD

Дата последнего изменения

Old_rule_name

Строка

Предыдущее имя правила для поиска по истории

Tags

Строка

Список тегов через запятую

License

Строка

Лицензия, под которой выпущено правило

Из всех необязательных полей score заслуживает отдельного разбора, потому что именно оно помогает расставить приоритеты при массовых срабатываниях.

Как выставлять Score?

Score отражает сразу два параметра: насколько серьезна угроза и как точно правило ее идентифицирует. Когда правил сотни и срабатываний десятки, аналитик может расставить приоритеты в реагировании по числовой оценке, не вчитываясь в каждое правило. Score 85+ означает, что совпадение с высокой вероятностью является реальной угрозой, а 30 указывает скорее на отдельный подозрительный признак, который стоит учитывать в совокупности.

Диапазон

Значимость

Примеры

0–39

Очень низкая

Возможности, упаковщики и т. п. (часто комбинируются для более высокой итоговой оценки)

40–59

Заслуживает внимания

Редкие упаковщики, аномалии в PE-заголовках

60–79

Подозрительный

Срабатывания эвристик, обфускация, общие правила

80–100

Высокий

Прямые совпадения с вредоносным ПО / хакерскими инструментами

Строки в правиле

С метаданными разобрались. Теперь к содержательной части. Секция strings определяет, что именно YARA будет искать в файле.

rule RULE_NAME {

    ...

    strings:

        $s1 = "value"

        $s2 = { E2 34 F1 67 }

        $r1 = /abc[def]+/

    ...

}

Здесь $s1 — это простая строка, $s2 — последовательность байтов, а $r1 —  регулярное выражение. Все рекомендации ниже относятся именно к этой секции и помогают сделать строки более читаемыми. 

Читаемость строковых значений

Одна из частых ошибок — записывать обычные текстовые строки в виде hex-последовательностей. Такую запись сложно прочитать без конвертации, и при ревью правила непонятно, что именно ищется.

Такое представление лучше не использовать:

$s1 = { 46 72 6F 6D 42 61 73 65 36 34 53 74 72 69 6E 67 28 }

Достаточно такого описания:

$s1 = "FromBase64String("

Исключение составляют строки с управляющими символами вроде \t или \n, для которых hex-формат предпочтителен.

Краткие идентификаторы

Похожая ситуация — это длинные, ничего не говорящие идентификаторы вроде $string_value_footer_1 или $selection_14. Когда таких переменных много и они используются в сложном условии, код становится трудночитаемым.

Лучше использовать краткие или описательные имена. Вот пример, как это выглядит на практике.

   $s1 = "eval("
   $eval = "eval("
condition:
   all of (s*) and $eval

Шестнадцатеричные идентификаторы

Для hex-значений, состоящих в основном из ASCII, полезно указывать строковое представление в комментарии.

/* )));\nIEX( */

$s1 = { 29 29 29 3b 0a 49 45 58 28 0a }

Длинные значения можно разбивать на сегменты по 16 Б. Так правило становится удобнее читать. В примере ниже продемонстрировал, как это может выглядеть.

$s1 = { 2c 20 2a 79 6f 77 2e 69 20 26 20 30 78 46 46 29 

        3b 0a 20 20 70 72 69 6e 74 66 20 28 28 28 2a 79 

        6f 77 2e 69 20 26 20 30 78 66 66 29 20 3d 3d 20 

        30 78 34 31 29 20 3f 20 22 4c 49 54 54 4c 45 5c 

        6e 22 20 3a 20 22 42 49 47 5c 6e 22 29 3b 0a 20 

        20 70 72 69 6e 74 66 20 28 22 73 68 6f 72 74 20 

        25 64 3b 20 20 69 6e 74 }

Категоризация строк по уровням $x*, $s*, $a*

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

  1. Высокоспецифичные строки ($x*). Уникальные индикаторы, почти всегда указывающие именно на искомую угрозу.

  2. Групповые строки ($s*). Не уникальны по отдельности, но значимы в комбинации.

  3. Строки предварительной выборки ($a*). Часто встречающиеся строки, сужающие тип/формат файла и оптимизирующие производительность поиска.

Разберем этот подход на примере реального правила. 

rule HKTL_Go_EasyHack_Oct23 {
   meta:
      description = "Detects a Go based hack tool"
      author = "John Galt"
      date = "2023-09-13"
      reference = "https://githoop.com/EdgyHackerFreak/EasyHack"
   strings:
      $a1 = "Go build"

      $x1 = "Usage: easyhack.exe -t [IP] -p [PORT]"
      $x2 = "c0d3d by @EdgyHackerFreak"

      $s1 = "main.inject"
      $s2 = "main.loadPayload"
   condition:
      uint16(0) == 0x5a4d
      and filesize < 20MB
      and $a1
      and (
        1 of ($x*)
        or all of ($s*)
      )
      or 4 of them
}

Здесь строки сгруппированы по типу и разделены пустыми строками для наглядности. Обратите внимание на логику условия. Достаточно одной высокоспецифичной строки ($x*), но если таких нет, то нужен весь набор групповых строк ($s*). Это компромисс между точностью и полнотой обнаружения.

Фильтры ложных срабатываний ($fp*)

Когда правило дает много ложных срабатываний, ему перестаешь доверять, а со временем и вовсе начинаешь игнорировать. Строки, указывающие на легитимные шаблоны, помечаются префиксом fp.

Если правило совпадает и с вредоносным паттерном, и со строкой $fp*, оно не срабатывает. Вот как это реализуется на практике.

rule HKTL_Go_EasyHack_Oct23 {
   meta:
      description = "Detects a Go based hack tool"
      author = "John Galt"
   strings:
      $a1 = "Go build"
      $s1 = "main.inject"
      $s2 = "main.loadPayload"
      $fp1 = "Copyright by CrappySoft" wide
   condition:
      uint16(0) == 0x5a4d
      and filesize < 20MB
      and $a1
      and all of ($s*)
      and not 1 of ($fp*)
}

Условие срабатывания правила

Если строки определяют, что искать, то условие отвечает на вопрос, при какой комбинации находок правило считается сработавшим. Слишком широкие или нестрогие условия ведут к росту ложных срабатываний, слишком узкие — к тому, что правило пропустит модификацию угрозы.

Рекомендуемый порядок проверок в условии и пример его оформления

rule RULE_NAME : TAGS {
    ...
    condition:
        header_check
        file_size_limitation
        other_limitations
        string_combinations
        false_positive_filters
}

Примеры оформления условий

Есть несколько приемов, которые делают условие более читаемым.

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

Второй — оборачивать части условия, объединенные через or, в отдельный блок с отступом.

rule RULE_NAME : TAGS {
    ...
    condition:
        uint16(0) == 0x5a4d
        and filesize < 300KB
        and pe.number_of_signature == 0
        and (
            1 of ($x*)
            or 3 of them
        )
        and not 1 of ($fp*)
}

Тот же принцип применим и для проверки нескольких возможных маркеров файла. Например, если правило должно работать и для Windows, и для Linux, проверку маркера тоже стоит обернуть в блок. 

rule RULE_NAME : TAGS {
    ...
    condition:
        (
            uint16(0) == 0x5a4d     // маркер MZ (Windows)
            or uint16(0) == 0x457f  // маркер ELF (Linux)
        )
        and filesize < 300KB
        and pe.number_of_signature == 0
        and all of ($s*)
        and not 1 of ($fp*)
}

Оптимизация. Строковое сопоставление вместо циклов

Последний раздел гайда касается производительности. Нередко для поиска паттернов в PE-заголовках используют циклы и хеширование:

condition:
   for any var_sect in pe.sections:
      (hash.md5( var_sect.raw_data_offset, 0x100 ) == "d99eb1e503cac3a1e90450d0c07e3ffc" )

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

Резюме

Единый стиль оформления YARA-правил нужен не для красоты — он напрямую влияет на то, насколько быстро аналитик поймет, что именно обнаружило правило, и насколько легко команда сможет поддерживать общий набор сигнатур. Ключевые вещи, которые, на мой взгляд, стоит забрать из гайда:

  • осмысленное имя от общего к частному;

  • обязательные поля метаданных;

  • три уровня специфичности строк ($x*/$s*/$a*);

  • фильтры ложных срабатываний ($fp*).

Комментарии (0)