Стоит изменить в файле хотя бы один байт — и его хеш станет другим. YARA ищет не точную копию, а заданный набор признаков, поэтому лучше подходит для обнаружения разных образцов малвари. В статье мы разберемся, как устроены правила и модули YARA, напишем собственные сигнатуры и применим их для поиска аномальных файлов.
YARA — это популярный инструмент для поиска и классификации малвари. Он работает на основе специальных правил, которые движок может применять к процессам и файлам. YARA создал Виктор Альварес (Victor Alvarez) во время работы в VirusTotal. Сегодня инструмент служит стандартом де‑факто для описания сигнатур вредоносных программ.
Понимание его внутренней работы дает представление о том, как устроены антивирусные сигнатуры и почему они выглядят именно так. Движок легко расширяется за счет пользовательских модулей на C, что позволяет добавлять произвольную логику анализа. На примере YARA легко проследить эволюцию антивирусных технологий: от простейших скан‑строк эпохи DOS до современных составных правил с вложенными условиями и данными от нативных модулей. В этой главе рассматривается YARA 4.5.8 — актуальная версия на момент подготовки текста.
Книга «Обход антивирусов на практике»
Это первая глава из книги «Обход антивирусов на практике».

Часть глав будет опубликована на сайте, а полная версия выйдет на бумаге. В первых частях рассматриваются алгоритмы детектирования, за ними следуют слепой поиск сигнатур и разбор методов обхода современных антивирусных движков.
Правила
В основе YARA лежит концепция правил (rules). Каждое правило описывает набор признаков — строк, последовательностей байтов, регулярных выражений, а также логических условий, при выполнении которых срабатывает детектирование. По сути, аналитик формализует свои знания о конкретном семействе малвари в виде правила YARA, после чего его можно автоматически применять к любым семплам.
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)
}
Например, это правило детектирует применение функций WinAPI, специализирующихся на расчете MD5-хеша. Оно ищет строчки с модификаторами wide ascii nocase — вариации с любым регистром, в однобайтовом (ASCII) или двухбайтовом (wide) бинарном представлении, после чего проверяет истинность условия (condition) — наличие хотя бы одной из двух DLL и всех указанных API.
Работу YARA можно разделить на компиляцию и сканирование. При компиляции движок разбирает правила, компилирует условия и извлекает из строк атомы — короткие последовательности байтов, по которым удобно быстро находить возможные совпадения. Из атомов строится автомат Ахо — Корасик. Мы рассмотрим его в одной из следующих глав. Во время сканирования он ищет эти последовательности в файле, после каждого совпадения проверяет полный строковый или байтовый шаблон, вычисляет условия и выводит сработавшие правила.
Сила YARA — в гибкости описания паттернов. В секции strings поддерживаются три типа сигнатур: шестнадцатеричные строки, текстовые строки и регулярные выражения. В синтаксисе YARA они называются просто строками. Каждый тип обладает набором конструкций и модификаторов, позволяющих описывать как точные, так и нечеткие совпадения.
Шестнадцатеричные строки
Шестнадцатеричные строки описывают последовательности байтов и служат основным инструментом для работы с бинарным содержимым — машинным кодом, заголовками файловых форматов, «магическими» и криптографическими константами. Записываются в фигурных скобках как пары шестнадцатеричных цифр, разделенных пробелами.
rule RawBytes
{
strings:
$hex = { E2 34 A1 C8 23 FB }
condition:
$hex
}
Четыре специальные конструкции — wildcard, отрицание, переходы и альтернативы — превращают примитивный побайтовый поиск в полноценный язык описания бинарных паттернов. Без них описание сигнатур байт‑кода было бы практически невозможным, потому что даже минимальные различия в сборке (другой компилятор, другая версия, другие флаги оптимизации) приводят к изменению конкретных байтов.
Wildcard (подстановочные символы)
Символ ? заменяет неизвестный полубайт (nibble). Wildcard работает именно на уровне полубайтов — это принципиально, поскольку позволяет зафиксировать старшую или младшую половину байта, оставив другую произвольной. Такая гранулярность нужна, например, при описании опкодов x86, где старший полубайт кодирует операцию, а младший — регистр.
$hex = { E2 34 ?? C8 A? FB }
Здесь:
- ?? — любой байт целиком (от 00 до FF);
- A? — старший полубайт равен A, младший — любой (от A0 до AF).
Инструкции x86 часто содержат переменные поля (ModR/M, SIB, displacement), и wildcard позволяет абстрагироваться от конкретных регистров или смещений. Например, инструкция mov 32-битного регистра кодируется байтами B8 — BF в зависимости от целевого регистра, а call — байтом E8 с четырьмя байтами относительного адреса. Запись B? здесь не подходит, поскольку она охватывает весь диапазон B0 — BF. Поэтому опкоды mov нужно перечислить явно:
( B8 | B9 | BA | BB | BC | BD | BE | BF ) ?? ?? ?? ??
Для call достаточно шаблона E8 ?? ?? ?? ??, где четыре байта wildcard заменяют относительный адрес.
NOT (отрицание)
Оператор ~ (тильда) перед байтом значит «любое значение, кроме указанного». Если wildcard — это «мне все равно, какой байт», то отрицание — это «мне все равно, какой байт, лишь бы не вот этот». Комбинация wildcard и отрицания позволяет исключать конкретные нежелательные варианты без необходимости перечислять все допустимые альтернативы.
$hex = { F4 23 ~00 62 B4 }
$hex2 = { F4 23 ~?0 62 B4 }
- ~00 — любой байт, кроме нулевого;
- ~0? — любой байт, у которого старший полубайт не равен 0.
Полезно для исключения конкретных вариантов инструкций или данных. Например, можно описать jmp short с ненулевым смещением (EB ~00) или последовательность байтов, где определенное поле не должно быть пустым. Отрицание также комбинируется с полубайтовым wildcard: запись ~?0 значит «младший полубайт не равен нулю», что покрывает 240 из 256 возможных значений байта вместо 255.
Jumps (переходы)
Подстановочные символы и отрицание решают проблему неизвестного содержимого при известной длине. Но бывают ситуации, когда неизвестна сама длина промежуточного фрагмента — между двумя значимыми паттернами может находиться от парочки до сотен байтов. Для этого существуют переходы (jumps) — диапазоны в квадратных скобках:
$hex = { F4 23 [4-6] 62 B4 }
Это значит: между F4 23 и 62 B4 может быть от 4 до 6 произвольных байтов. Допустимые формы:
| [4-6] | От 4 до 6 байт |
| [10-] | От 10 байт до бесконечности |
| [-] | От 0 байт до бесконечности |
Переходы критически важны в нескольких типичных сценариях.
Во‑первых, компилятор может вставлять выравнивание (padding) и инструкции NOP между функциями, и их количество непредсказуемо.
Во‑вторых, между двумя ключевыми инструкциями в функции часто находится переменное количество промежуточного кода — другие флаги оптимизации сгенерируют другое количество инструкций.
В‑третьих, данные в структурах файловых форматов могут иметь поля переменной длины (строки, массивы), и переход позволяет «перепрыгнуть» их, привязавшись к маркерам начала и конца. Неограниченный переход [-] — это, по сути, утверждение «эти два паттерна встречаются в файле в указанном порядке на произвольном расстоянии».
Alternatives (альтернативы)
Конструкция ( ... | ... ) задает несколько допустимых вариантов для фрагмента. Синтаксис заимствован из регулярных выражений: варианты перечисляются через | внутри круглых скобок.
$hex = { F4 23 ( 62 B4 | 56 | 45 ?? 67 ) 45 }

1 неделя назад
7







English (US) ·
Russian (RU) ·