Написать рабочее 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-процентной вероятностью указывают на конкретную угрозу, другие полезны только в комбинации, а третьи нужны по большей части для предварительной фильтрации. Автор гайда предлагает трехуровневый подход, в котором каждый уровень обозначается своим префиксом:
Высокоспецифичные строки ($x*). Уникальные индикаторы, почти всегда указывающие именно на искомую угрозу.
Групповые строки ($s*). Не уникальны по отдельности, но значимы в комбинации.
Строки предварительной выборки ($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*).