Измени один байт в файле — и хеш станет другим. YARA ищет не точную копию, а набор признаков, поэтому лучше подходит для обнаружения разных образцов вредоноса. Разберёмся, как устроены правила и модули YARA, напишем собственные сигнатуры и применим их для поиска аномальных файлов.
Что такое YARA
YARA — инструмент для поиска и классификации вредоноса на основе специальных правил. Движок применяет их к процессам и файлам. Создатель Виктор Альварес разработал YARA во время работы в VirusTotal. Сегодня это стандарт де факто для описания сигнатур вредоносных программ.
Понимание его внутреннего устройства показывает, как работают антивирусные сигнатуры и почему они выглядят именно так. Движок расширяется за счёт пользовательских модулей на C, позволяя добавлять произвольную логику анализа.
На примере YARA видна эволюция антивирусных технологий: от простых скан-строк эпохи DOS до современных составных правил с вложенными условиями и данными от нативных модулей.
Правила
В основе YARA лежат правила. Каждое описывает набор признаков — строк, последовательностей байтов, регулярных выражений, а также логических условий, при выполнении которых срабатывает детектирование. Аналитик формализует свои знания о конкретном семействе вредоноса в виде правила YARA, после чего его можно автоматически применять к любым образцам.
Пример правила, которое детектирует применение функций WinAPI для расчета MD5-хеша:
rule MD5_API {
meta:
author = '_pusher_'
description = 'Looks for MD5 API'
date = '2016-07'
strings:
$advapi32 = 'advapi32.dll' wide ascii nocase
$cryptdll = 'cryptdll.dll' wide ascii nocase
$MD5Init = 'MD5Init' wide ascii
$MD5Update = 'MD5Update' wide ascii
$MD5Final = 'MD5Final' wide ascii
condition:
($advapi32 or $cryptdll) and ($MD5Init and $MD5Update and $MD5Final)
}
Правило ищет строки с модификаторами wide и ascii nocase — вариации с любым регистром в однобайтовом (ASCII) или двухбайтовом (wide) представлении. Затем проверяет условие: наличие хотя бы одной из двух DLL и всех указанных API.
Работа YARA делится на две фазы: компиляция и сканирование. При компиляции движок разбирает правила, компилирует условия и извлекает из строк атомы — короткие последовательности байтов для быстрого поиска. Из них строится автомат Ахо-Корасик. Во время сканирования он ищет эти последовательности в файле. После каждого совпадения проверяет полный строковый или байтовый шаблон, вычисляет условия и выводит сработавшие правила.
Сила YARA — в гибкости описания паттернов. Секция strings поддерживает три типа сигнатур: шестнадцатеричные строки, текстовые строки и регулярные выражения. Каждый тип имеет конструкции и модификаторы для описания как точных, так и нечётких совпадений.
Шестнадцатеричные строки
Шестнадцатеричные строки описывают последовательности байтов и служат основным инструментом для работы с бинарным содержимым — машинным кодом, заголовками файловых форматов, криптографическими константами. Записываются в фигурных скобках как пары шестнадцатеричных цифр, разделённых пробелами.
rule RawBytes {
strings:
$hex = { E2 34 A1 C8 23 FB }
condition:
$hex
}
Четыре специальные конструкции превращают примитивный поиск в полноценный язык описания бинарных паттернов: wildcard, отрицание, переходы и альтернативы. Без них описание сигнатур байт-кода было бы невозможным, потому что даже минимальные различия в сборке приводят к изменению конкретных байтов.
Wildcard (подстановочные символы)
Символ ? заменяет неизвестный полубайт. Это работает на уровне полубайтов — это позволяет зафиксировать старшую или младшую половину байта, оставив другую произвольной. Такая гранулярность нужна, например, при описании опкодов x86, где старший полубайт кодирует операцию, а младший — регистр.
$hex = { E2 34 ?? C8 A? FB }
Здесь ?? — любой байт целиком (от 00 до FF), а A? — старший полубайт равен A, младший — любой (от A0 до AF).
Инструкции x86 часто содержат переменные поля, и wildcard позволяет абстрагироваться от конкретных регистров или смещений. Например, инструкция mov 32-битного регистра кодируется байтами B8—BF в зависимости от целевого регистра, а call — байтом E8 с четырьмя байтами относительного адреса.
( B8 | B9 | BA | BB | BC | BD | BE | BF ) ?? ?? ?? ??
Для call достаточно шаблона E8 ?? ?? ?? ??, где четыре байта wildcard заменяют относительный адрес.
NOT (отрицание)
Оператор ~ перед байтом означает «любое значение, кроме указанного». Если wildcard — это «мне всё равно, какой байт», то отрицание — это «мне всё равно, какой байт, лишь бы не вот этот». Комбинация позволяет исключать конкретные нежелательные варианты без необходимости перечислять все допустимые альтернативы.
$hex = { F4 23 ~00 62 B4 }
$hex2 = { F4 23 ~?0 62 B4 }
~00 — любой байт, кроме нулевого, а ~0? — любой байт, у которого старший полубайт не равен 0.
Полезно для исключения конкретных вариантов инструкций или данных. Например, можно описать jmp short с ненулевым смещением (EB ~00) или последовательность, где определённое поле не должно быть пустым.
Jumps (переходы)
Подстановочные символы и отрицание решают проблему неизвестного содержимого при известной длине. Но бывают ситуации, когда неизвестна сама длина промежуточного фрагмента — между двумя значимыми паттернами может находиться от парочки до сотен байтов. Для этого существуют переходы — диапазоны в квадратных скобках:
$hex = { F4 23 [4-6] 62 B4 }
Это означает: между F4 23 и 62 B4 может быть от 4 до 6 произвольных байтов.
Допустимые формы:
- [4-6] — от 4 до 6 байт
- [10-] — от 10 байт до бесконечности
- [-] — от 0 байт до бесконечности
Переходы критичны в нескольких типичных сценариях. Компилятор может вставлять выравнивание и инструкции NOP между функциями, и их количество непредсказуемо. Между двумя ключевыми инструкциями в функции часто находится переменное количество промежуточного кода — разные флаги оптимизации генерируют разное количество инструкций. Данные в структурах файловых форматов могут иметь поля переменной длины, и переход позволяет «перепрыгнуть» их, привязавшись к маркерам начала и конца.
Alternatives (альтернативы)
Конструкция ( ... | ... ) задаёт несколько допустимых вариантов для фрагмента. Синтаксис заимствован из регулярных выражений: варианты перечисляются через | внутри круглых скобок.
$hex = { F4 23 ( 62 B4 | 56 | 45 ?? 67 ) 45 }
