Автор: it_solutions
Специально для форума xss.pro
В данной статье мы рассмотрим популярный в последнее время антивирусный движок YARA и скриптовый язык YARA, применяющийся для описания сигнатур детектируемых объектов.
Будет подробно и пошагово рассмотрен процесс сборки приложения, которое, используя YARA-движок, будет проверять файлы и процессы по заданным нами правилам, написанным на языке YARA.
Приложение мы будем собирать максимально компактным и быстрым, с отключенным CRT и зависимостями. А также почищенным от конфиденциальной информации, которую туда добавляет компилятор от майкрософт.
Так же будет рассмотрена работа с YARA-движком на Питоне.
Подробно сконцентрируемся на первостепенных практических вопросах: как собрать EXE с YARA-движком, написать "Hello World" на YARA и тому подобных.
Так же можно эту статью рассматривать как методичку для быстрого старта и ознакомления с технологией YARA.
До появления антивирусных движков каждый антивирус имел свой определённый набор баз со своим проприетарным форматом и свои собственные реализации процедур поиска сигнатур в файлах.
Причём всё это могло отличаться не только в разных антивирусах, но и в разных релизах одного и того же антивируса.
Постоянно вносимые изменения в основные алгоритмы работы антивирусов и полное отсутствие стандартизации отрицательно сказывалось на качестве антивирусного программного обеспечения.
Кроме того, подобный бардак в алгоритмах и базах сигнатур делал невозможным интеграцию антивирусов с другим программным обеспечением, например с IDS и EDR системами.
Поэтому компанией VirusTotal (да, именно та компания, которая предоставляет широко известный чекер файлов) был разработан единый стандарт работы антивирусного программного обеспечения, и называется он YARA.
YARA представляет собой специализированный язык, описывающий типовой набор средств и операций для поиска и детектирования вредоносного программного обеспечения.
На каждый детектируемый вредоносный объект составляется YARA-правило, содержащее в себе такие признаки детектирования, как статические и динамические сигнатуры, контрольные суммы, энтропию и много другое.
В результате этого для того чтобы сделать детектирование определённого вредоносного файла отпадает необходимость писать программный код. Всё, что нужно для детекта, можно описать с помощью простого YARA-скрипта.
YARA движок создан для того чтобы маскимально упростить детектирование вредоносных объектов, а так же стандартизировать работу программ, предназначенных для обеспечения информационной безопасности (антивирусов, IDS и EDR систем)
YARA движок позволяет сканировать как определённый файл, так и определённый участок памяти.
Функция скана памяти даёт возможность детектировать криптованные файлы - будучи распакованными в памяти они становятся не защищёнными криптом, и YARA движок их детектит ровно так же, словно и никакого крипта не делалось.
Таким функционалом, например, обладает NOD32. Из этой статьи будет понятно, как этот антивирус делает детект файлов в памяти и почему ему не страшен стандартный крипт, как RunPE так и LoadPE.
Наверное многие с такой проблемой сталкивались - криптуем файл, в статике ни одного детекта, в том числе и от нода. Но после запуска файла Nod определяет криптованный файл <b>ровно под тем же именем, что и до крипта</b>
Как будет далее видно из данной статьи, YARA движок может использоваться не только в антивирусном программно обеспечении.
Будучи очень компактным и быстрым, и написанным на C++, он легко интегрируется с любым другим программным обеспечением.
Например, с помощью YARA-движка мы можем автоматизировать поиск файлов на компе, содержащих строки "password" или "login".
Или поиск определённых последовательностей цифр, например таких как номера CC, состоящие из 16 цифр в определённом формате, построенному по алгоритму Luhn.
Учитывая возможность YARA-движка сканировать определённую область памяти мы можем сделать обход всех запущенных процессов на компе с последующим их открытием и доступом к используемой ими оперативной памяти.
И далее, используя YARA-правила, искать в памяти этих процессов нужные нам данные.
Например, была когда-то распространена POS-малварь (AlinaPOS, DexterPOS и т.д.), искавшая в памяти процессов все последовательности цифр, похожие на номера кредитных карт, сверяя их по алгоритму Luhn.
К данной малвари очень был бы актуален подключенный YARA движок, с его возможностями сканировать определённый участок памяти, применяя к нему то или другое YARA правило.
Так же существуют довольно продвинутые стилеры, которые тянут инфу не с парольных кешей или с баз SQLite браузеров, а непосредственно из областей памяти, используемой запущенными процессами данных браузеров.
Тут так же будет актуален YARA движок. С помощью него можно создать простой и эффективный скрипт, описывающий что именно нужно искать в памяти процессов браузеров.
Кроме этого всего, с помощью YARA движка можно реализовать мощный инструмент для анализа памяти компьютера.
Используя огромные возможности YARA-движка по поиску определённых паттернов, сигнатур и строк можно хорошо автоматизировать поиск и анализ нужных данных в оперативной памяти компьютера.
Для начала сделаем простейшую программу, ищущую с помощью YARA движка определённую текстовую строку в файле, путь к которому передаётся через командную строку.
Эта текстовая строка будет задана отдельным YARA правилом, которое передаётся YARA движку.
Данная программа должна быть максимально компактной (размер EXE килобайт примерно 100), не должна иметь DLL-зависимостей, должна работать как stand-alone exe
Кроме этого, в дальнейшем, мы переведём этот EXE в формат небольшой компактной DLL. Так как сейчас более популярен этот формат исполняемых файлов.
Сначала мы сделаем вариант с хранением YARA-правил в отдельных файлах на диске, в файлах с расширением *.yara
И далее переделаем в бесфайловый вариант, с хранением YARA-правил внутри EXE/DLL и считыванием этих правил YARA движком из памяти, без использования файлов.
YARA поставляется в трёх вариантах: в виде исходного кода, в виде DLL файлов и в виде статической библиотеки.
Нас интересует только статическая библиотека - именно этот вариант позволяет разместить YARA движок полностью внутри исполняемого EXE файла.
Вариант с DLL нам неинтересен и сразу же отбрасывается - нельзя с собой таскать дополнительные DLL файлы.
DLL (сокращение от Dynamic Link Library) это динамический вариант библиотек, что подразумевает под собой возможность замены или изменения библиотеки, например по причине обновления версии библиотеки.
Поэтому динамический вариант библиотек это всегда отдельный дополнительный файл. Статический вариант не предполагает в дальнейшем какого-то изменения или замены библиотеки, поэтому библиотека вшита внутрь исполняемого файла, и никакого дополнительного файла на диске не требуется (это удобный для нас вариант).
Кратко о статических и динамических библиотеках можно сказать так: динамическая библиотека это возможность замены или обновления библиотеки, статическая это быстрота, компактность и отсутствие дополнительных файлов.
Статические библиотеки для Visual Studio, которые прилинковываются напрямую к исполняемому файлу без использования DLL, имеют расширение *.LIB.
В нашем случае статическая библиотека из комплекта поставки YARA будет там лежать под именем libyara32.lib, собственно это один из двух файлов оттуда которые нас интересует для текущей задачи.
Второй файл это файл yara.h, который содержит в себе прототипы функций, вызываемых из библиотеки libyara32.lib
Больше никаких файлов из комплекта поставки YARA нам не понадобятся. Только вот эти два - libyara32.lib и yara.h
Эти два файла можно найти в приаттаченном к данной статье демонстрационном исходнике, иллюстрирующим работу с YARA.
Подключать libyara32.lib лучше не в настройках проекта, а директивой #pragma, так удобней:
Это лучше чем прописывать библиотеку где-то в настройках проекта. Прописав таким способом библиотеки где-то вначале исходника они всегда будут на виду.
И чтобы поменять что-то в подключаемых библиотеках не нужно будет где-то лазить в меню проекта и искать нужные вкладки.
Прописав подключаемые библиотеки в исходнике с помощью директив препроцессора #pragma, мы всегда будет держать их на виду, централизованно и в одном месте.
Далее прописываем *.h файл с прототипами вызова функций YARA
Всё, с этого момента мы готовы к работе с библиотекой YARA из среды C++.
Чтобы запустить и проинициализировать YARA-библиотеку мы сначала вызываем функции yr_init и yr_create_context
В случае успешного завершения операции вернётся текущий контекст, либо нулевое значение в случае ошибки.
Контекст нужен для многопоточной работы, для каждого отдельного потока он будет свой.
Этот контекст передаётся параметром во все функции библиотеки YARA, для того чтобы YARA движок мог обеспечить многопоточную работу, это механизм межпоточного взаимодействия.
По этому контексту YARA определяет, из какого потока произошёл вызов той или иной процедуры, и какому потоку отдать данные, которые обработала та или иная функция.
Контекст представляет собой структуру, в которую можно писать настройки YARA движка, применительно для того потока которому этот контекст соответствует.
Там пишутся такие настройки, как список файлов на проверку, список YARA-правил, коды ошибок и т.д.
После завершения работы с YARA движком мы должны удалить созданный контекст, воспользовавшись функцией yr_destroy_context
Функции передаётся единственный параметр - указатель на контекст, с которым мы завершаем работу.
Мы пропишем в контекст одну из главных вещей - обработчик ошибок в YARA-правилах.
Прописывается обработчик ошибок таким образом:
Где report_error это callback-функция следующего вида:
На входе она принимает имя файла с YARA-правилом, номер строки, где возникла ошибка, и описание возникшей ошибки.
То же самое, как и при обработке любым компилятором синтаксических ошибок в исходнике - тот так же пишет ошибку и указывает на место в файле, где эта ошибка произошла.
Это очень нужная вещь при отладке YARA-правил, без этого не обойтись - обязательно нужна обратная связь от YARA-движка о правильности синтаксиса написанных нами YARA-правил.
Рассмотрим YARA-правило для простейшего детекта, который ищёт определённую сигнатуру в файле и при нахождении этой строки YARA движок сигнализирует о детекте.
Сигнатура - это некоторая короткая, неизменная последовательность байт, присутствующая в детектируемом объекте.
Сигнатура переводится с английского как подпись (signature) - на детектируемом объекте как бы написана определённая последовательность байт, говорящая о том, что это за объект. В этом заключается смысл этого названия.
Сделаем простейший сигнатурный детект на языке YARA для файлов, запакованных пакером UPX.
Правило будет выглядеть так:
Сначала пишем ключевое слово rule, обозначающее начало правила.
За ним следует название правила - "Packed_UPX"
В фигурных скобках будет содержаться текст этого правила.
Правило делится на секцию данных (в данном примере это секция с именем strings) и на секцию условий (секция с именем conditions).
Имя секции заканчивается двоеточием, за именем секции на следующей строке пишется содержимое этой секции.
Секции данных могут быть различного типа, например строки, последовательности байт, хэши и т.д.
Здесь, в нашем простейшем примере, мы пишем секцию данныx типа "strings", в которой перечисляются только строковые сигнатуры.
Указываем в ней строку "UPX!", и присваиваем её переменной $s1, в этой переменной дальше и будет храниться эта сигнатура.
Строка UPX! присутвствует во всех файлах, запакованных пакером UPX, поэтому мы её и выбрали в качестве сигнатуры.
На рисунке зелёным цветом обведено место в запакованном файле, которое мы выбрали в качестве сигнатуры пакера UPX.
Эта сигнатура всегда находится в начале кодовой секции, обычно расположена первой по счёту.
Перед сигнатурой видны чиcла "3.06" - так UPX записывает номер своей версии в запакованный файл.
Далее в секции condition мы перечисляем условия, при которых срабатывает данное правило и происходит детект.
Там мы пишем условия с именем $s1 - это строка, которую мы прописали в секции strings.
Если в секции strings было указано несколько строк, то перечисляем их имена переменных через запятую.
Так же в секции condition указываются переменные, содержащие в себе последовательности байт, хеши и другие более сложные типы данных.
Если будет найден хотя бы один элемент, прописанный в секциях даннных, то правило считается сработавшим и YARA движок выдаёт детект.
Полную официальную документацию по языку YARA можно найти здесь: https://yara.readthedocs.io/en/stable/
В официальной документации будут примеры только по поиску и детектированию различных опасных объектов.
В данном цикле статей по YARA-движку и InMemory-стилерам будет много примеров по поиску и детектированию различной конфиденциальной информации в различных объектах - файлах, участках памяти и т.д.
Дальше это простейшее YARA правило подключаем к нашему тестовому проекту, демонстрирующему написание "hello world" для YARA движка.
Сохраняем только что написанное нами YARA правило в файл с именем test.yara
Для того, чтобы отправить этот файл YARA движку, пишем следующий код:
Сначала мы открываем файл "test.yara" целиком в память стандартной crt функцией fopen, которая вернёт хэндл файла,, либо ноль в случае ошибки.
Далее мы используем функцию yr_compile_file, которая на входе получает хэндл только что открытого файла, а так же хэндл контекста, полученный при инициализации YARA-движка.
В случае успешного завершения функция yr_compile_file вернёт нулевое значение, в случае ошибочного завершения вернёт код ошибки.
После вызова этой функции и компиляции YARA-правил все подготовительные действия с YARA-движком завершены.
На этом этапе YARA-движок проинициализирован, правила скомпилированы и переданы ему - движок полностью готов к детектированию объектов.
Поэтому дальше рассматриваем передачу YARA-движку проверяемого объекта и сверку его с YARA правилами.
Пишем дальше следующий код:
Прочитываем полностью в буфер памяти с именем fileBuffer проверяемый объект (в нашем случае некий EXE файл, либо запакованный, либо не запакованный UPX).
После того как объект прочитан в память, проверяем его функцией для проверки файлов YARA-движком, это функция с именем yr_scan_mem
Первым параметром она принимает область памяти, содержащую в себе целиком проверяемый объект.
Вторым параметром идёт размер этой области памяти. Здесь мы указываем стандартную функцию _msize из stdlib, возвращающую размер выделенной памяти по указателю на неё.
Третим параметром идёт ранее рассмотренный нами хэндл контекста.
Пятым параметром идёт имя файла - YARA не будет что-то читать из этого файла или делать какие-либо другие операции с этим файлом. Это просто строка с именем файла, для генерации отчёта - файл с таким-то именем чист или есть детект.
Четвёртым параметром идёт callback-функция обработки объекта, в параметрах который передаётся результат ранее скомпилированных YARA-правил.
На этой callback-функции остановимся подробнее.
Данная функция обратного вызова первым параметром принимает структуру RULE, которая содержит статус обработанного правила и другую дополнительную информацию о процессе обработки данного YAКA-правила.
Из этой информации нам интересен параметр flags, по которому мы определяем применилось ли данное правило или нет. Проверяем мы этот параметр, применив логическую операцию "И" над этим параметром и константой RULE_FLAGS_MATCH (которая определена в yara.h).
Если есть совпадение правила - значит выводим сообщение о найденном детекте, в противоположном случае печатаем, что детекта нет.
В случае нахождения детекта мы печатаем имя файла, взяв его из параметра callback функции с именем buffer, и идентификатора (имени) детекта, взяв его и структуры rule.
Имя (идентификатор) детекта - это то, что мы указывали в YARA-правиле после идентификатора rule, в нашем случае это строка "Packed_UPX":
По выходу из процедуры обратного вызова могут быть два варианта.
Первый вариант это возвращение значения CALLBACK_CONTINUE. В этом случае YARA-движок осуществляет проверку следующего YARA-правила и снова вызывает callback функцию для этого правила.
Во втором случае возвращается любое другое значение, отличное от CALLBACK_CONTINUE. В этом случае дальнейшая проверка правил для данного объекта прекращается.
После завершения работы с YARA-движком и перед выходом из программы YARA-движок нужно обязательно выключить, вызвав функцию yr_destroy_context.
На первый взгляд это кажется необязательным - всё равно программа завершается, и все ранее открытые хэндлы файлов, потоки и всё остальное уничтожатся вместе с процессом, из которого мы вышли.
Но на самом деле это не так.
Часто можно встретить такую ошибку - по выходу из программы иногда появляется сообщение об ошибке, код которой либо "MEMORY ACCESS VIOLATION" либо "INVALID HANDLE".
То есть перед самым выходом где-то произошло обращение к уже освобождённой памяти или к закрытому хендлу.
И это происходит достаточно редко: раз 20 например программа завершается нормально а на 21 раз эта проблема появляется и потом надолго исчезает снова.
Эту ошибку, появляющуюся таким непредсказуемым образом, очень тяжело отловить. То ошибка есть, то её нету.
Поэтому, перед выходом из программы, нужно обязательно останавливать все запущенные потоки, закрывать все хендлы, освобождать всю память. Несмотря на то, что якобы винда должно сама всё это закрывать по завершению процесса.
Для этого, по завершению работы с YARA-движком, и нужно вызывать функцию yr_destroy_context. Это предотвратит подобные трудноотлавливаемые ошибки.
Функция yr_destroy_context корректно закроет всё, что наоткрывал и назапускал YARA-движок: закроет все хендлы, высвободит всю память, остановит все запущенные потоки и т.д.
В конечном итоге, собрав всё вышеописанныек вещи воедино, мы получаем короткую программу, которая через использование YARA-движка проверяет файл заданный в командной строке на соответствие правилу, написанному на языке YARA, и хранящемуся в файле test.yara
Этот исходник лежит в приаттаченном файле yara_hello_world.zip
В предыдущем примере мы сделали хранение YARA-правил в отдельном файле test.yara
Это удобно для освоения языка YARA и экспериментов с ним. В этом файле можно писать свои правила и применять его к различным файлам, задаваемым в командной строке. Осваивать элементы языка YARA: ключевые слова, структуру правил, типы данных и т.д.
Всё это удобно для изучения, тестирования и отладки.
Но хранение YARA-правил в отдельном файле хорошо только для учебно-отладочного варианта, в боевом варианте отдельный файл недопустим - всё должно храниться внутри EXE/DLL файла.
Мы всегда должны стремиться к бесфайловому режиму работы, поэтому стремимся избежать даже создания временных файлов на диске.
Рассмотрим следующий код:
Сначала считываем целиком файл test.yara в буфер памяти tmpMem
Далее, используя функцию YARA движка yr_compile_string, компилируем это правило.
Этой функции передаётся два параметра: указатель на буфер памяти, где хранятся YARA-правила, и хэндл контекста. Этот хендл контекста мы получаем на этапе запуска и инициализации YARA-движка и мы рассмотрели его устройство в начале статьи.
Слово string (строка) в этой функции не должно смущать - здесь под строкой понимается любая последовательность печатных ASCII символов, заканчивающихся нулём.
Эта строка может быть и в мегабайт размером.
Символы переводы строки CR/LF не считаются окончанием такой строки, они являются частью этой строки.
Поэтому файл с YARA правилами, написанными построчно, всё равно считается в этом случае одной строкой.
После компиляции функцией yr_compile_string YARA-правил, буфер памяти, содержащий прочитанный файл test.yara, больше не нужен. Поэтому высвобождаем его с помощью функции free.
В этом примере мы по прежнему не ушли от использования файла test.yara, но использовали другую функция YARA-движка, которая компилирует правила не из отдельного файла, а из памяти.
Компилирует принимает указатель на строку с правилами, которая хранится где-то в памяти, а не в файле на диске.
Теперь всё, что нам остаётся сделать, это найти способ хранения данной строки с YARA-правилами внутри EXE файла.
Если yara-правило короткое, то его можно просто сохранить в виде строки:
Так это будет выглядеть внутри скомпилированного EXE:
Зелёным цветом обведено YARA-правило, то как оно выглядит внутри скомпилированного EXE файла.
За ним видны дополнительные строки, используемые в программе: диагностические сообщения, строки внутри YARA-движка и т.д.
Строка с YARA-правилом идёт первой, потому что она быда первой упомянутой строкой с исходнике.
Компилятор помещает строки в исполняемый файл в порядке их упоминания в исходнике.
Отсюда следует важный вывод - положением строк внутри исходника можно управлять. Например, мы хотим убрать строку от начала секции данных, чтобы она меньше бросалась в глаза.
Тогда в исходнике прописываем эту строку самой последней. Или прописываем в середине общего массива строк, если хотим размесить строку в середине секции данных внутри EXE.
Но если размер файла с YARA-правилами составляет, например, десятки килобайт или даже больше, то способ записи в виде строки в исходнике не подойдёт.
Поэтому YARA-правила в исходнике мы будем записывать не в виде строк, а в виде байтового массива.
В байтовый массив файлы можно переводить, используя встроенную в windows утилиту certutil
Набираем в командной строке:
В результате выполнения данной команды мы получим файл test.hex со следующим содержимым:
Удаляем в текстовом редакторе обведённые зелёным цветом области, оставляем только область с шестнадцатеричными цифрами.
Далее редактируем эту оставшуюся область, приводим её к следующему виду
Константа yara_size равна размеру файла, и она, так же, соответствует размеру байтового массива, в котором мы храним YARA правило.
Теперь содержимое файла test.yara находится в этом байтовом массиве.
В скомпилированном EXE файле этот байтовый массив хранится так же как и предыдущий варант со строкой.
Компилятор не делает различия в способе хранения этих данных внутри EXE - и строка из предыдущего примера и этот байтовый массив в EXE хранятся идентичным образом, и даже по одному и тому же адресу.
Таким способом внутри EXE можно хранить и любые другие файлы, не только YARA-правила как в нашем случае. Этот способ универсален.
Итак, мы избавились от необходимости хранения YARA-правил в отдельном файле на диске.
Вместо этого они теперь хранятся в секции .data внутри EXE-файла.
Передать их YARA-движку можно теперь таким образом:
Код теперь упростился всего лишь до одной строки. Тут просто передаём функции yr_compile_string указатель на байтовый массив, в котором мы поместили точную копию содержимого файла test.yara
Этого метода хранения данных внутри EXE/DLL файлов нет нигде в паблике.
Метод хорош тем, что позволяет уменьшить размеры EXE/DLL файла за счёт того, что в нём не будет использоваться секция хранения данных .data
Вместо станартного хранения данных в секции .data мы их будем помещать в ту область файла, где хранится DOS-заглушка.
Для начала что это вообще такое: DOS-заглушка в EXE/DLL файлах.
Открываем любой EXE/DLL файл и в самом начале мы видим следующее:
На скриншоте зелёным цветом обведён MZ (Legacy) заголовок и жёлтым цветом PE (Portable Executable) заголовок.
Белым цветом закрашена опасная область EXE файла с конфиденциальной информацией - Rich сигнатура.
Она всегда расположена между Legacy заголовком и PE-заголовком.
В конце Rich-заголовка видна его сигнатура - строка "Rich". Это сокращение от "Richard Shupak" - имя одного из разработчиков PE-формата исполняемых файлов.
Подобно тому как MZ это сокращение от имени "Mark Zbikowski" - имя разработчика формата исполняемых файлов под MS-DOS.
Как видно, в EXE/DLL файлах не один, а два заголовка. Первый заголовок это заголовок для MS-DOS, той операционной системы которая была до Windows.
Этот первый заголовок находится в самом начале файла и содержит в себе 16-битный исполняемый файл для совместимости с системой MS-DOS.
Удивительно, но этот древний архаизм есть в абсолютно любом исполняемом EXE/DLL файле. Он поддерживается и, скорее всего, далее будет поддерживаться всеми версиями Windows.
Стандартным действием этого MS-DOS файла при запуске в данной операционной системе является вывод сообщения "This program cannot be run in DOS mode." и завершение работы.
Мы же будем делать нестандартное действие.
Пишем на ассемблере следующий код:
Этот код скомпилируется в 16-битный EXE файл для системы MS-DOS. Он будет той DOS-заглушкой, которую мы вставим в наш EXE/DLL файл.
Первой строкой здесь идёт директива "format mz", это означает что мы компилируем исполняемый файл формата MS-DOS
Следующии 4 строки формируют стандартный заголовок MS-DOS файла.
Из них стоит обратить внимание на строку entry zz:entry_point - это определение точки входа, места откуда стартует программа.
И в этом месте мы пишем не код программы, а размещаем свои данные !
После метки entry_point мы видим YARA-правило, которое введено на ассемблере с помощью директив резервирования байтов DB (Define Byte).
Компилируем этот исходник следующий командой:
Получившийся файл dos_stub.bin будет выглядеть так:
Вначале идёт 16 битный Legacy заголовок, сразу за ним хранятся наши данные.
Область начала хранения наших данных начинается со смещения 0x20 (обведено зелёным цветом).
В нём мы видим наше YARA-правило, хранящееся по смещению 0x20 от начала файла.
Теперь подключаем эту DOS-заглушку к нашему EXE/DLL файлу с YARA-движком.
В Visual Studio открываем меню опции проекта, открываем дополнительные параметры линкера и пишем там строку: "/STUB:dos_stub.bin"
И кладём файл dos_stub.bin в корневую папку проекта:
Компилируем файл и открываем его в HEX-редакторе, видим там следующее:
Наши данные теперь хранятся в самом начале файла, а не стандартным способом в отдельной секции с именем .data !
Но на скриншоте мы видим один неприятный момент (обведено красным цветом) - по смещению 0x3C записаны какие-то четыре непонятных байта, что повреждает наши данные.
Эти четыре байта (F0 00 00 00) это записанное в обратном порядке число 0x000000F0 (числа в PE заголовках).
Число записано в заголовке в обратном порядке потому, что таков стандарт записи данных в x86 процессоре. Числа пишутся от младщего разряда к старшему.
Отсюда и название этого стандарта хранения чисел little-endian, что дословно переводится как отсчёт с малого конца.
Поэтому в этих четырёх байтах записано число 0xF0, отсчитываем от начала файла 0xF0 байт и видим что там расположен PE-заголовок.
Значит, по этому адресу линкер прописывает смещение PE-заголовка в файле (по адресу 0x3C от начала файла).
Мы это должны учитывать, поэтому будем размещать данные в DOS заглушке начиная со смещения 0x40. Сразу же за тем местом, где хранится указатель на PE-заголовок. Так мы избежим порчи наших данных линкером.
В DOS-заглушке наши данные хранятся по смещению 0x20 от начала файла. Безопасное смещение, где данные точно не перезапишутся, начинается с адреса 0x40
Поэтому первые 0x20 байт в точке входа DOS-заглушки мы заполним нулями, в результате этого YARA-правило расположится в EXE/DLL файле ровно по смещению 0x40 от начала файла.
Исходник DOS-заглушки теперь будет выглядеть так:
Единственное отличие от предыдущего варианта это то что мы поставили директиву "times 0x20 db 0" после метки entry_point
Эта директива резервирует в файле 0x20 нулевых байт. Так мы отодвинули повреждающуюся область от наших данных.
Компилируем эту DOS-заглушку, подключаем её к нашему EXE с YARA-движком, и компилируем его.
Открываем полуившийся EXE в HEX-редакторе и видим следующее:
На скриншоте красным цветом обведена перезаписываемая область (четыре байта) и зелёным цветом обведено новое смещение начала хранения данных.
Белым прямоугольником закрашена Rich-сигнатура EXE, содержащая в себе конфиденуиальную информацию о системе, на которой был скомпилирован файл.
Наше YARA-правило записалось теперь без повреждений.
Как получить теперь доступ к этим данным изнутри запущенного EXE файла ?
Первое что нужно сделать, это получить адрес памяти, по которому загружен EXE файл. То есть получить значение ImageBase.
Делается это с помощью вызова винапи GetModuleHandle(0)
GetModuleHandle(0) вернёт указатель на начало EXE файла в памяти, к нему мы добавляем значение 0x40. Добавляем значение смещения в файле, по которому лежат наши данные в DOS-заглушки.
В памяти оно будет ровно таким же, как и в файле.
В конечном итоге, указатель на данные из DOS-заглушки на C++ будет выглядеть вот так:
И далее передаём этот указатель функции yr_compile_string
Да, обращение к данным лежащим в DOS-заглушке будет крайне неудобным, но за счёт этого достигается уменьшение размера EXE-файла.
Этот способ имеет смысл, если у нас есть какие-то крупные данные, обозначенные всего лишь одной переменной.
Например, набор YARA-правил в десятки килобайт размером или какой-то дроппер в виде исполняемого файла.
Если у нас данные разбиты на множество мелких переменных, то их хранение в DOS-заглушке будет неудобным.
Например, набор из множества строк, в которых хранятся сообщения об ошибках - в DOS-заглушке такое хранить неудобно, для каждой строки надо будет искать смещение от начала файла.
И если мы изменим одну строку, то тогда изменятся смещения всех строк, расположенных сразу же за ней. И для этих строк нужно будет определять новые значения.
Ресурсы в EXE/DLL файлах были специально придуманы для того, чтобы была возможность обновить некие данные, используемые программой, не затрагивая самого программного кода.
Например, в программе все сообщения выводятся на английском языке, нужно их заменить на немецкий - как сделать замену всех строк с сообщениями ?
Для этого нужно организовать некое централизованное хранилище этих строк, и нужен некий программный интерфейс, который выдаёт программе доступ к каждой строке с сообщением.
В этом случае мы можем централизованно заменить все строки, и не заботится о том что строке будут разной длины, из-за чего изменятся указатели на эти строки в программе.
Программный интерфейс, работающий с этим централизованный хранилищем, возьмёт на себя работу по правильному размещению строк в хранилище, выдаче программе их адресов и многое другое.
Этот програмный интерфейс встроен в систему Windows и называется <B>ресурсами исполняемых файлов</B>.
Ресурсы хранятся в EXE/DLL файле в отдельной секции, обычно с именем .rsrc
Туда часто размещают строки, графику, иконки, мультимедию и т.д. То есть всё то, что предполагается к периодической замене в программе. Но при этом замену нужно сделать без внесения каких-либо правок в программный код и перекомпиляции EXE.
Например, нам нужно заменить логотип в нашей программе. Логотип хранится в виде BMP файла в ресурсах EXE/DLL файла, считывается оттуда и отображется при старте программы.
Редактором ресурсов заменяем этот логотип на другой - такая замена происходит никак на затрагивая программный код.
Наборы YARA-првавил это постоянно меняющаяся вещь. Постоянно добавляются какие-то новые объекты, обновляются правила для старых объектов, удаляются объекты ставшими неактуальными для детектирования.
Такие обновления YARA-правил могут делать по-несколько раз в сутки.
Поэтому каждый раз вносить изменения в программный код и перекомпилировать EXE это плохой вариант, так как со времнем гарантированно добавит ошибок в программном коде.
В то же время мы не хотим хранить YARA-правила в отдельных файлах, нам нужно чтобы был stand-alone exe, чтобы всё хранилось внутри одного EXE файла.
Поэтому здесь будет очень актуальным использовать механизм ресурсов, и хранить YARA-правила в секции ресурсов внутри EXE/DLL файла.
И как только мы хотим сделать обновление YARA-правил мы добавляем их в EXE с помощью редактора ресурсов, без перекомпиляции исполняемого файла и вмешательства в программный код.
Чтобы добавить в ресурсы EXE/DLL набор правил, содержащийся в файле yara_rules_list.yara, подключаем в Visual Studio *.rc файл. Это стандартный файл с описанием подключаемых к EXE ресурсов.
Редактируем его так как показано на скриншоте:
В первую очередь, на скриншоте красным цветом обведён один очень важный момент.
Визуал студия по умолчанию прописывает в ресурсах идентификатор той локали, которая установлена в данный момент на компе !
И в основном по этой записи и спаливается страна происхождения файла. В этом месте можно прописать нужную локаль, например USA.
Зелёным цветом здесь обведена запись, которую нужно добавить.
Вот этот код отдельно:
Тип ресурса STRING выбран потому, что YARA-правила представляются в ресурсах как одна большая ASCIIZ строка.
Наше тестовое YARA-правило в *.rc файле записываем следующим образом:
1. Строка пишется внутри кавычек сразу после идентификатора
2. Символы перевода строк записываем в конце каждой строоки как \r\n
3. Для удобства перенос строк в *.rc файле делается символом "\"
4. Символ табулюяции прописывается как \t
Компилируем EXE, открываем получившийся файл и находим там секцию ресурсов. Видим там следующее:
Строка с YARA-правилом, записанная в секции ресурсов, обведена зелёным цветом.
А так же на скриншоте хорошо видно то, какой огромный объём избыточной инофрмации добавляется в скомпилированные PE файлы - все те области забитые нулями не используются системой.
В них можно размещать свои данные и это никак не отразиться на работоспособности EXE - это дополнительный задел для оптимизации скомпилированных EXE/DLL файлов.
Так же мы видим, что строка записывается в ресурсы в формате Wide Char (то есть по два символа на байт, второй байт в данном случае всегда ноль).
Чтобы получить эту строку в первоначалном ASCII формате, мы получаем её с помощью апи LoadStringA. Если мы хоти получить её в том формате как она сейчас хранится в ресурсах, тогда это делаем с помощью апи LoadStringW.
Чтобы получить эти YARA-правила из ресурсов пишем следующий код:
Первым делом нам нужно определить размер строки с YARA-правилом, хранящимся в ресурсах. Потому что нам нужно выделить память под строку, и необходимо знать количество памяти, которое нужно выделить.
Сначала находим ресурс со строкой по его идентификатору IDS_YARA_RULES с помощью апи FindResourceEx
В вызове этой апи всё очевидно, кроме указания идентификатора строки: (IDS_YARA_RULES >> 4) + 1
Дело в том, что строки ресурсах хранятся в блоках по 16 записей размером. Система считывает и записывает в ресурсы EXE строки не по одной, а пакетами по 16 строк в каждом.
Чтобы идентифицировать отдельную строку внутри каждого блока мы должны воспользоваться вышеприведённой формулой.
В этой формуле мы берём идентификатор строкового хранилища IDS_YARA_RULES и делим его на 16 ("IDS_YARA_RULES >> 4" - эта то же деление на 16, только более оптимизированная её запись).
Затем прибавляем к получившимуся результату единицу, указатель на первую запись в блоке. Там и будет храниться наша строка с YARA-правилом.
Ещё один важный момент с функцией LoadString. В интернете полно примеров когда LoadString возвращает длину строки, если ей указать последним параметром ноль.
Этот способ больше не работает начиная с WIndows10, и в MSDN написано что значение всегда должно быть отличающееся от нуля !
Под Windows10 LoadString вернёт в этом случае не длину, а нулевое значение (то есть ошибку).
Теперь разберёмся, насколько можно уменьшить размер только что собранного нами тестового примера, удалив из EXE всё лишнее.
Во-первых, мы можем отбросить прилинковываемую статическую библиотеку C++ stdlib, и заменит её на стандартную библиотеку msvcrt.dll из поставки windows.
Делается это так: сначала в настройках указываем "использовать динамическую DLL".
Скомпилировав EXE файл, мы сразу же добъёмся резкого уменьшения размера, так как вся RTL будет убрана из EXE и вместо этого теперь будет хранится в отдельной DLL.
Но тут есть подвох. Если мы запустим этот EXE на компе с установленной Visual Studio, то всё нормально сработает.
Но на компе, где Visual Studio нет, будет вот такой результат:
То есть система не может найти DLL, которая прописывается в систему при установке Visual Studio (MSVCR71.dll)
Как это исправлять ?
Открываем наш EXE файл в hex-редакторе и идём в таблицу импорта, в нём мы видим строку с именем этой проблемной DLL
И вместо этой строки пишем там вот это: msvcrt.dll
Лишние символы, если будут, просто забиваем нулями
В результате таблица ипорта будет выглядеть теперь так:
Пробуем запустить отредактированный таким способом EXE на компьютере без Visual Studio - всё теперь запускается.
Вместо DLL стандартной библиотеки, для какой-то определённой версии Visual Studio, мы указали универсальную стандартную библиотеку msvcrt.dll, которая есть во всех версиях Windows
В нашем случае эта библиотека не будет ничем отличаться от той библиотеки, которую прописала в EXE визуал студия.
Базовый набор функций в этих библиотеках полностью идентичен, а какие-то специфические новые функции, которые есть в новых библиотеках crt, мы использовать не планируем.
И поэтому смело прописываем в EXE универсальную CRT библиотеку Windows.
В результате, после всего этого редактирования, получаем очень компактный EXE размером всего лишь 118784 байт.
В этом очень небольшом размере получилось разместить полноценный антивирус, работающий на YARA-движке !
Первое что нужно удалить из боевой версии EXE это отладочную информацию. Кроме того, что эта информация облегчает анализ файла, она ещё хранит в себе разную конфиденциальную информацию.
Открываем EXE в hex-редакторе и набираем в поиске сроку "RSDS", откроется следующая область:
Здесь мы видим, что после строки RSDS идут какие-то загадочные 16 байт, и сразу за ними строка с путём к PDB файлу. Обычно это идентично той папке, в которой лежит проект.
Это на самом деле очень мощная подстава от Visual Studio, например в том случае если проект расположен в пользовательской папке (а по умолчания Visual Studio предлагает сохранять проекты именно там).
Таким способом, юзернейм попадает внутрь EXE файла.
Ещё хуже дело обстоит с 16 байтами, которые идут после строки RSDS.
Эти байты представляют собой GUID, которых как известно, нунжо опасаться по той причине, что они могу генерироваться на основе MAC-адреса сетевой карты.
Подробнее об этой проблеме здесь: https://www.computerworld.com/article/2541891/faq--for-microsoft--antipiracy-is-a-naked-baby.html
Там про то, как майкрософт прятал в гуидах внутри документов офиса MAC адреса. Для того чтобы выявлять пиратские копии.
Поэтому, отладочной информации в EXE файле быть не должно.
Открываем свойства проекта, и выбираем "Generate debug info: No"
С отладочной информацией внутри EXE разобрались, смотрим дальше.
Открываем внутри HEX редактора сведения о PE-заголовке. В HIEW, например, это делается нажатием клавиши F8
Видим следующее:
Первое что мы там видим это дата компиляции EXE, что весьма неконфиденциально (обведено зелёным цветом).
А так же мы там видим версию линкера, которым собирался EXE и который установлен у нас на компе (обведено жёлтым цветом).
Нам нужно удалить эту информацию из PE-заголовка, делается это следующим образом.
Переходим к началу заголовка, и отсчитываем от него 8 байт:
В этом месте PE-заголовка хранится timestamp (обведено зелёным цветом), то есть время и дата компиляции EXE.
Забиваем эти 4 байта нулями и открываем снова просмотр заголовка EXE:
Мы видим, что дата компиляции сменилась на 1 января 1970 года.
Эта дата в PE-заголовке является начальной точкой отсчёта времени, а timestamp представляет собой 32 битной число, содержащее в себе количество секунд, прошедшее с этой даты.
Много ли это или мало, и когда этих четырёх байт перестанет хватать для записи количества прошедших секунд ?
Считаем: одно 32 битное число равняется 2^32=4294967296
Делим это на количество секунд в сутках: 4294967296 секунд / 86400 секунд в сутках = 49710 суток
Делим количество суток на 365: 49170 суток / 365 суток в году = 136 лет
Отсюда получается, что как минимум до 2106 года время в заголовках PE-файлов будет обрабатываться корректно.
Дальше удаляем версию линкера: идём так же к началу PE-заголовка отсчитываем 26 байт и видим там следующее:
Видим два байта 07 и 0A (обведено зелёным цветом) что соответствует десятичным числам 7 и 10 и, соответсвенно, версии линкера 7.10
Так же затираем нулями и снова открываем просмотр заголовка PE:
Видим, что дата сборки EXE и версия линкера теперь удалены, PE заголовок почищен от конфиденциальной информации.
Теперь перемещаемся в самое начало файла и находим там так называемую Rich-сигнатуру.
Эта та область, которая на всех предыдущих скриншотах постоянно перекрывалась белым цветом.
Последовательность байт со строкой "Rich" в самом конце, расположенная между MZ и PE заголовками.
Там мы видим некую зашифрованную обдасть данных. В ней просто изобилие конфиденциальной информации: версия студии, компилятора, серийный номер, какие-то гуиды т .д.
Эту Rich-сигнатуру всегда смело забиваем нулями. На работоспособности EXE это вообще никак не отразится. Ничего, кроме очевидных проблем, эта запись в EXE файле не приносит.
Зелёным цветом здесь обведена удалённая (забитая нулями) Rich-сигнатура. На этом месте, кстати, можно разместить какие-то свои данные.
Например, под RSA-ключ там места вполне хватит, если записывать его в бинарном виде.
Так же стоит сказать об именах секций в PE-заголовке.
Смотрим на этот скриншот:
Зелёным цветом здесь обведены имена секций .text, .rdata, .data, .rsrc
Если эти имена секций забить нулями, то это вообще никак не отобразиться на запуске самого EXE.
Под каждое такое имя секции в PE заголовке отведено 8 байт, таким образом для четырёх секций в данном PE-файле зарезервировано 32 байта, в которые можно записывать что угодно, и мы можем там хранить какие-то свои данные.
Приплюсуем к этому четыре байта таймстемпа и два байта версии линкера, и получаем, что в данном PE-заголовке мы можем сохранить 32+4+2=38 байт какой-то своей информации.
Как уже здесь упоминалось, в EXE/DLL файлах содержится очень много избыточной информации. Они сильно разряжены и там встречается много пустого места, заполненного нулями.
А также там есть много областей, игнорируемых загрузчиком Windows. Туда можно записать какую-то свою информацию и на работоспособности файла это никак не отразится.
Даже в самом PE-заголовке в данном примере нашлось целых 38 байт свободного места.
В приаттаченом к статье файле getrich.cpp находится исходник дампера Rich-сигнатур.
Исхоник не мой, взят вроде бы из исходников утилиты moloch, но точно уже не помню откуда.
С помощью него можно ради интереса попроверять разные EXE/DLL файлы - в каких-то будет много содержаться конфиденциально инфы, в каких-то меньше.
И после этого появится представление о том, что будет если эту сигнатуру не удалять из своих EXE файлов.
Проблема с удалением Rich-сигнатуры может быть в том, что файлы без неё могут считаться потенциально опасными объектами.
Rich-сигнатуру, например, очень любит проверять Avira. Причём она не просто проверяет факт её наличия, но и корректнсоть и правильный её формат. Если Rich сигнатуры нет, авира, как правило, выдаёт детект типа HEUR/Generic.
Поэтому существует более сложный, но более правильный путь, чем просто забивание Rich-сигнатуры нулями - это подделка Rich-сигнатуры. Но это уже тема отдельной статьи.
Так же в языке YARA встроена возможность анализа и парсинга Rich-сигнатур.
Парсер Rich-сигнатур находится в модуле с именем "pe", а доступ к расшифрованной Rich-сигнатуре записывается в YARA одной строкой: pe.rich_signature.clear_data
Кроме того, парсер Rich сигнатур возвращает такие данные как длину, ключ, версию Rich-сигнатуры и многое другое.
Самый полезная функция это, конечно же, clear_data - она возвратит расшифрованную Rich-сигнатуру и в ней уже можно организовать побайтовый или построчный поиск.
Для тех, кто уже давно работает в сфере MALDEV, все эти неприятные вещи с Visual Studio давно уже известны.
Но если читаете впервые про то, что в EXEшники попадает какая-то конфиденциальная инфа при их сборке, то это всё будет экстремально важной и полезной информацией.
Чистить всё это вручную не так уж и сложно, но можно и написать каких-то простых скриптов на питоне, автоматизирующих все вышенаписанные действия.
Я предпочитаю чистить вручную - а вдруг в скрипте были какие-то ошибки и в результате что-то там не почистилось, лучше не лениться и проделать все эти операции вручную.
Тем более, что это не так уж много времени занимает.
Можно обойтись и без редактирования EXE, отведя под сборку EXE-файлов отдельный комп, на котором точно нет ничего конфиденциального и он всегда отключен от сети.
Отдельный комп неудобен, но хорош по той причине, что мы не можем точно знать - а есть ли ещё какие-то другие способы утечки конфиденциальных данных через сборку EXE ? Что ещё, куда, и каким способом визуал студия может помещать в бинарник ?
А ещё лучше, собирать на таком отдельном компе, с последующим удалением вручную всех вышеперечисленных проблемных мест.
Для компиляции используем всегда старые версии студий. Тут есть два проверенных варианта: VS2003/2005 и VS2010
Первая умеет делать очень компактные 32 битные EXE и мало добавляет метаинформации в скомпилированные файлы.
Вторая умеет собирать 64 битные EXE, но 32 битные EXE получаются крупнее и уже больше добавления метаинформации.
Поэтому. если EXE 32 битный (это будет в большинстве случаев) предпочтительнее использовать VS2003/2005, если предполагается делать 64 битный то тогда VS2010
Если нам нужно добавить то, чего нету в старых версиях визуал студии, то просто подключаем к ним библиотеку BOOST. В бусте есть абсолютно всё.
Например, в VS2003 нету поддержки регулярных выражений - тогда ставим boost и используем "<boost/regex>".
Ещё ряд дополнитеьных способов, как "утрамбовать" EXE.
Можно объединить секцию с программным кодом и секцию данных - так будет ещё дополнительный выигрыш в размере.
Ставим минимильное выравнивание секций (параметр FileAlign) - выигрываем ещё пару килобайт размера.
Так же, кроме стандартной библиотеки, мы можем выбросить стандартный код C++ для инициализации программы. Указав вместо стандартной точки входа main свою собственную.
В этом случае пропадёт доступ к аргументам командной строки через переменные argv/argc, но доступ к командной строке всё равно останется, например через апи GetCommandLina(A|W)
Сделать всё это можно добавив в исходник следующую строку:
Этой строкой мы указываем дополнительные опции компилятору.
Команда /MERGE объединяет три секции ".rdata", ".data" и ".text" в одно целое - теперь и данные и код будут лежать в одной секции с именем .text
По умолчанию на секции .text стоят только атрибуты RW, это означает что запись в область памяти этой секции запрещена, а это создаёт нам неудобства.
Поэтому, командой /SECTION указываем для секции .text аттрибуты доступа ERW (Execute,Read,Write), что означает полный доступ к памяти на выполнение, чтение и запись.
Параметром /FILEALIGN мы задаём самое минимальное значение выравнивания секций, которое может быть (512 байт, по умолчанию оно равно 4096 байтам).
И параметром /ENTRY мы указываем свою точку входа вместо стандартной точки входа main. Этим действием мы отбрасываем crt startup код, тем самым экономим место.
И финальное действие, идём во вкладку "Опции компилятора", и задаём там максимальную степень оптимизации кода.
С включенной полной оптимизацией выигрываем ещё 1-2 Kb в размере файла.
Итак, на данном этапе мы добились минимального размера EXE-файла и почистили его от содержащейся в нём конфиденциальной информации.
Теперь финальный этап: паковка EXE-файла. Сделаем это с помощью паковщика UPX и отредактируем запакованный файл так, чтобы его нельзя было распаковать обратно этим же пакером.
Набираем в командной строке:
Параметр "-9" означает уровень степени сжатия, указываем максимальный, девятый.
После паковки файл ужмётся примерно в два раза.
Такой хороший уровень сжатия достигается из-за того, что EXE файлы содержат в себе много повторяющихся нулей и других повторяющихся последовательностей данных. А такие повторяющиеся данные хорошо сжимаются.
Обратно файл можно распаковать слеюующей командой:
Параметр "-d" означет decompress, то есть распаковку.
Сделаем так, чтобы запакованный файл нельзя было распаковать этой командой.
Открываем запакованный файл в HEX редакторе и ищем там строку "UPX!"
И просто забиваем её нулями.
Пробуем распаковать отредактированный таким способом пакованный EXE файл той же командой: upx -d test_yara.exe
И получаем следующий ответ от пакера UPX о невозможности распаковки:
Зелёным цветом здесь обведено сообщение пакера UPX о невозможности распаковки и о том что файл возможно был модифицирован.
От профессионального реверсера данное действие, конечно же, не спасёт, но как очень быстрый и простой способ защиты EXEшника вполне сгодится.
Итак, на данном этапе, мы получили очень компактный файл размером в 30 килобайт, который умеет делать детект объектов по правилам, написанным на языке YARA.
Если мы добавим рекурсивную проверку всех файлов на диске, а не только проверку файла указанного в командной строке, то мы фактически получим полноценный антивирус, размер которого будет всего-лишь 30 Kb.
И будет он сканировать файлы с огромной скоростью, за счёт того что оттуда выкинуто всё лишнее, а так же остутствует GUI, который на себя забирал бы огромную часть вычислительных ресурсов.
Антивирус с YARA-движком сделали, и смотрим дальше - а можно ли вместо антивируса сделать, например, стилер с YARA движком ?
Допустим нам необходимо найти тестовый файл с определённым содержимым, найти его либо рекурсивным обходом всех файлов на диске, или, например, отслеживая открытие любых *.txt файлов.
Например, нас интересует файл, в котором хранится список логинов и паролей.
Нам известно, что логин там начинается со строки "Login:" и пароль со строки "Password:".
Таких записей там несколько сотен, по этой причине признаком данного файла с паролями мы считаем нахождение в файле каждой из строк "Login:" и "Password:" более пяти раз - для того чтобы убрать ложные детекты.
Кроме этого, нам известно что у интересующего нас файла гарантированно большой размер, поэтому мы отсекаем все мелкие файлы. Мелкими файлами мы будем считать файлы, размер которых меньше 5 килобайт.
Пишем простое YARA-правило для отслеживания файла с перечисленными признаками:
Мы создали YARA-правило, при срабатывание которого YARA-движок будет выдавать детект с именем fileWithPasswords.
В секции данных strings иы перечисляем строки, наличие которых мы проверяем в файле.
В секции condition мы пишем условие для проверки количества нахождений этих строк в файле.
Для обозначения количества нахождений переменной в проверяемом объекте в YARA перед имнем переменной ставится символ "#"
В данном примере, #loginStr будет являться числом, равным количеству строк, содержащемся в проверяемом нами *.txt файле.
Проверка условия представляет собой логическое выражение, которое своим синтаксисом похоже на оператор в языке Python.
Приоритет операций (сравнение ">" выполняется первым, логическая операция and выполняется после сравнения), порядок расстановки скобок - всё то же самое, как в питоне.
Ключевое слово filesize в языке YARA представляет собой размер проверяемого файла (если проверяем область памяти, то это ключевое слово вернёт значение 0), в данном условии мы не детектируем файлы, размер которых меньше чем 5 килобайт.
Результат работы данного YARA-правила - будет выдаваться детект на все проверяемые файлы (объекты), в которых будут найдены строки "Login:" и "Password:" более 5 раз, и размер этого файла будет превышать 5 килобайт.
Оговорка про слово "объект" не случайна - объектом может быть не только файл, но и, например, определённый участок памяти, или фрагмент передаваемых по сети данных.
То есть мы можем поймать данный интересующий нас список паролей не только путем отслеживания и поиска файлов на диске, но и путём мониторинга памяти запущенных процессов либо мониторинга сетевой активности.
Данное YARA-правило отражает собой основной принцип работы целого класса защитного программного обеспечения, именуемого DLP-системами.
DLP расшифровывается как Data Leakage Protection, что переводится как предотвращение утечки данных.
В настройках DLP систем прописываются определённые правила для защищаемых объектов (очень часто на рассматриваемом нами языке YARA), после чего DLP система отслеживает все объекты, подпадающие под данные правила.
Например, DLP система отследила объект попавший под определённое YARA-правило и была зафиксирована попытка отправки данного объекта куда-то по сети - DLP система выдаёт сообщение о нарушении безопасности.
Если вместо выдачи сообщения о нарушении безопасности мы отправляем этот файл куда-то в админку на дедике, то это будет не DLP-система, а система противоположная ей - малварь.
Отсюда делаем важный вывод: принцип работы у DLP-систем и у малвари один и тот же, разница есть только в реакции на обнаруженные данные - либо их отправка с компа либо наоборот, предотвращение отправки
Это означает, что мощную DLP-систему можно использовать как мощный стилер, и любую опенсорсную DLP-систему можно переделать в гибко настраиваемый и конфигурируемый стилер.
В YARA имеется возможность подключения различных дополнительных модудей и плагинов.
В базовом наборе модулей, поставляемых с YARA-движком, сгруппированы различные наборы операций и инструментов, для анализа и детекта какого-то определённого типа объектов.
Одним из таких важнейших модулей является модуль для анализа исполняемых файлов, то есть файлов типа EXE/DLL.
Подключается он в YARA таким образом:
Подключение дополнительных модулей в YARA делается с помощью ключевого слова "import" и следующего за ним имени модуля.
Ключевое слово.import всегда указывается вне области правила, то есть не внутри фигурных скобок и не между строкой с ключевым словом rule и фигурными скобками.
Кроме того, здесь указан модуль time, позволяющий получить текущую дату и время на компьютере.
Данное правило иллюстрирует один очень неприятный детект, с которым сталкиваются те, кто занимается криптом EXE.
В секции условия детекта в данном YARA-правиле проверяется дата и время компиляции EXE/DLL файла (именно то, что мы удаляли из EXE файла, когда чистили EXE от конфиденциальной информации, которую там оставляет линкер).
Затем, с помощью функции time.now(), мы получаем текущее время в секундах. И вычитаем из него время компиляции EXE, которое хранится в значении pe.timestamp
Затем получившуюся разницу мы сраниваем с 86400 секундами (количество секунд в одних сутках, 60 секунд*60 минут*24 часа=86400 секунд).
Если разница получилась меньше, значит детектим данный файл как скомпилированный менее суток назад. Такие файлы AV всегда держит под подозрением.
Конечно, свежесобранный файл AV не записывает сразу в малварь, но на такие файлы делается особо тщательная проверка.
И в результате вылезают те детекты, которых могло бы и не быть, если бы в заголовок PE был бы правильно скорректирован timestamp.
Профилактика такого детекта очень простая - в виртуалке, или на том отдельном компе где происходит финальная сборка, должна быть установленна дата и время, сильно отстающая от текущей. Например на год меньше текущей даты.
Это короткое YARA-правило демонстрирует всю мощь YARA-движка. Написав всего лишь 5 строчек мы задетектили огромную выборку потенциально опасных файлов.
Детектирование опасных объектов с помощью YARA или аналогичных движков сильно упрощается, в этом и заключается ответ на вопрос, почему после крипта файлы теперь начинают детектится через 2-3 часа.
Ещё один пример, детектор оверлеев в EXE/DLL файлах:
Здесь, в секции проверки условий, мы сверяем фактический размер файла с размером файла, прописанным в PE-заголовке.
Загрузчик Windows, обрабатывая PE-файл перед запуском, считает размером EXE/DLL файла именно то значение, котоое указано в поле заголовка pe.size_of_image
То, что находится на диске за пределами этого размера, загрузчиком Windows игнорируется.
Там программа может хранить какие-то свои данные. Например SFX-архивы там хранят свои запакованные файлы. Или малварь хранит свой блок настроек.
Если есть превышение фактического размера файла на диске и его размером в памяти, считается что в конец файла записан оверлей.
Наличие оверлея всегда вызывает подозрение у AV - это очень редко встречающаяся ситуация у легетимных файлов. Из легетимных файлов с оверлеем это, в основном, различные SFX-архивы и некоторые инсталляторы.
В остальных случаях такой файл считается подозрительным и для него производится особо тщательная проверка, так же как и в только что рассмотренном случае со свежим таймстемпом в PE-заголовке.
Ещё пример, определяющий страну сборки исполняемого файла.
Данный пример описывает детект на все EXE/DLL файлы, которые собирались в Российской Федерации.
Функция pe.language берёт идентификатор локали из секции ресурсов PE-файла. В этой секции, по умолчанию, всегда прописывается текущая локаль компа.
Она называется LCID и представляет собой шестнадцатеричное число, в двнном случае этой число 0x19, что означает Российская Федерация.
Новые версии линкеров любят прописывать по умоланию секцию ресурсов .rsrs в собираемый файл, даже в том случае, если никаких ресурсов программа использовать не будет.
Просто это секция делается с минимальным размером и пустой. Но идентификатор локали там остаётся. В результате, по этой секции можно определить страну.
Данный детект обходится так же просто - на том компе или виртуалке, где происходит финальная сборка, ставим английскую винду. Тогда локаль в ресурсах будет прописываться как 0x09 (English).
Тут вырисовывается та же проблема, что и у вбивщиков кредитных карт.
Вбивщику необходимо максимально замаскироваться под холдера: поставить английскую версию винды, выставить правильное время на компе и часовой пояс и сделать ещё кучу настроек, чтобы комп, с которого идёт вбив, максимально был похож на комп холдера.
Теперь, при финальной сборке EXE/DLL, мы оказываемся в том же положении, что и этот вбивщик.
Для чистки от детектов, для финальной сборки EXE/DLL нужен отдельный комп или виртуалка. Туда нужно поставить английскую винду, английскую версию визуал студии, откатывать системное время на компе на год назад и делать многие другие вещи, чтобы скомпилировыанные EXE были максимально похожие на легитимные.
Это спасёт от первичных детектов, от тех признаков которые AV проверяют в первую очередь. Время сборки, подозрительная страна сборки и т.д.
Утилит YARA так же присутствует в дистрибутиве Kali Linux.
Набираем в командной строке "yara":
В случае если yara не установлена, система спросит, установить лия yara или нет, скачав её из репозитория.
Нажимаем "Y" и ждём завершения процесса установки:
После установки утилитой YARA пользоваться можно так, набрав в командной строке:
Здесь file_with_rules_list.yara это файл с YARA-правилами, file_for_test это тестируемый файл.
Это простейший способ вызова утилиты YARA, полный список её возможностей можно получить набрав в командной строке "yara --help".
Данная утилита очень удобна для отладки YARA-правил до прописывания их в готовом продукте (EXE/DLL файле).
Отладили этой утилитой разные YARA-правила, оттестировали на нужных файлах, убедились что всё нормально работает - после этого прописываем их внутри нашего EXE/DLL.
Очень удобно с помощью данной утилиты нализировать большое количество вредоносных файлов.
Например, у нас в каталоге лежит несколько тысяч EXE файлов.
Нам, например, нужно отфильтровать оттуда лишь те, размер которых менее 10 килобайт, скомпилированных не ранее, чем неделю назад, и страна происхождения которых USA.
Для этого пишем такое YARA-правило:
И сохраняем его в файл file_filter.yara
Далее запускаем утилиту YARA таким способом:
Здесь символ "*" обозначает проверить все файлы в текущем каталоге.
После запуска утилита yara выдаст построчный список файлов, подходящих под прописанные в yara-правиле условия.
Для тех, кто программирует на Python, будет хорошей новостью что для него доступен модуль YARA, подключение и настройка которого намного проще, чем в уже рассмотренном нами варианте с C++.
На Python очень удобно делать прототипирование работы с YARA движком (и вообще любое прототипирование, Питон для этого подходит просто идеально).
Собрали прототип на Питоне. Убедились что определённая задумка, выполненная в виде прототипа, вполне себе работает.
И после этого прототип, написанный на Питоне, переводим уже на любой нейтив код, например на C++.
Установка YARA стандартная, через менеджер пакетов PIP:
Далее можно пользоваться пакетом YARA из среды Python, пишем простейшую программу на питоне для работы с YARA-правилами:
Первым делом импортируем библиотеку c YARA движком, написав "import yara"
Второй строкой компилируем YARA-правила, содержащиеся в файле test_rule.yara с помощью функции yara_compile
Функция yara.compile возвратит объект rules, с помощью которого можно делать различные операции над только что скомпилированными правилами.
Одной из таких операций является применение правил к определённому файлу, что мы и сделали в четвёртой строке, вызвав метод match объекта rules с именем файла testfile.exe
Метод match возвращает список имён задетектенных объектов. Если детектов нет, то возвращается пустой список.
Сделаем тестовый файл testfile.exe и потестируем всё это на нём.
В текстовом редакторе набирем некоторое количество случайных букв, и где-то среди них размещаем две строки: "UPX!" и "12345"
Тестовый файл testfile.exe выглядит так:
Зелёным цветом здесь обведены места в файле, которые мы будем детектировать с помощью YARA-движка.
Будем считать, что эти две строки являются сигнатурами, и при нахождении таких строк в файле YARA должны выдавать детекты с именами Packed_UPX и Bytes_12345
Набор YARA-правил для этого случая, состоящий из двух правил, будет выглядеть так:
В правиле Packed_UPX мы сигнатуру указали как строку.
В правиле Bytes_12345 мы строку "12345" указали в байтовой форме - это сделано для того, чтобы продемонстрировать то, как в YARA прописывается тип данных "последовательность байт".
Последовательность байт пишется в фигурных скобках, в виде набора шестнадцатеричных чисел, отделённых друг от друга пробелами.
Результат работы данного скрипта с этим YARA-правилом будет выглядеть так:
На экран выводится список из строк, элементами которого являются строки с именами детектов, то есть имена YARA-правил которые сработали на тестируемом файле.
Если мы применим данный скрипт к другому файлу, где такие строки остутствуют, то скрипт вернёт пустой список:
YARA-движок в Питоне так же умеет проверять не только файлы, но и запущенные процессы, находя их по идентификатору PID (Process ID)
Сейчас сделаем следующее: напишем YARA-правило для реального EXE-файла, и сделаем его детект в памяти, детект уже запущенного процесса.
Это так же будет хорошей демоснтрацией того, как работает NOD32 с его детектами в памяти. В NOD32, скорее всего, встроена не YARA, но очень похожая по принципу работы система.
Так же мы проделаем все действия по прописыванию детекта, которые делают работники вирлаб, когда им попадаёт объект который они должны добавить себе в базы.
Конечно же, в очень сильно упрощенном виде, но основа действий будет понятна.
Делать это мы будем на файле putty.exe
Откроем файл putty.exe в HEX-редакторе и ищём строку, которую назначим сигнатурой. Нам надо найти такую строку или последовательность байт, которая встретится только в детектируемом файле и не встретится в любом другом.
Чтобы не было ошибочного детекта.
Подходящая последовательность байт изображена на этом скриншоте:
[
На скриншоте зелёным цветом обведена последовательность байт со строкой "PuTTY" и следующим за ней нулевыми байтми. Её мы и выберем в качестве сигнатуры.
Делаем YARA правило для этой последовательности байт:
Сохряняем данное правило в файл putty_detect.yara и дальше работаем с ним через Python
Далее пишем скрипт, который задетектит в памяти <b>запущенный процесс</b> putty.exe (но не сам файл putty.exe)
Скрипт выглядит так:
Здесь всё так же, как в предыдущем скрипте для детекта файла, только вместо имени файлы мы указываем pid=некоторое_число
Где это значение PID взять ?
Получаем его так: сначала запускаем putty.exe и в командной строке вызываем следующую команду:
Команда tasklist используется для получения списка процессов и информации о каждом из них (аналог команды ps в линуксах)
Если набрать tasklist без параметров, то он выведет информацию о всех процессах, запущенных в системе.
На нужна информация только о процессе с именем putty.exe
Поэтому в параметрах команды tasklist мы указали фильтр для выводимой информации, так чтобы отображался только процесс putty.exe
Фильтр выводимой информации в команде tasklist указывается с помощью параметра "/FI" и следующего за ним в кавычках описателя фильтра.
В данном случае это строка "imagename eq putty.exe"
imagename означает тип фильтруемого элемента (в данном случае фильтрация по имени процесса).
eq означает "равно", это сокращение от слова "equal" (равенство)
И дальше, после равно, указывается имя нужного нам процесса (putty.exe)
Итак, результат работы этой команды будет выглядеть так:
Значение PID, полученное с помощью этой команды, прописываем в скрипт на Питоне.
Запускаем скрипт yara_test.py, получаем следующий результат:
Вот это всё, что мы только что проделали - так примерно и выглядит работа сотрудника антивирусной лабы, в рабочие обязанности которого входит добавление объектов в базы.
В очень сильно упрощённом виде, в реальности таких детектов с одной строкой в yara-правиле не бывает. Но принцип один и тот же.
Только что разобранный пример с детектом EXE файла на Питоне это было реальное тестовое задание при устройстве на работу в одну из AV лаб.
Задание было сформулировано так: в zip архиве лежит набор из нескольких файлов, нужно сделать детектирование этих файлов на Питоне.
Детект должен быть сделан как для файлов на диске, так и для процессов, запущенных из этих файлов.
Список рабочих инструментов такой: Питон, YARA, hex-редактор.
YARA в Питоне не установлена, установить её надо самостоятельно (поэтому здесь было расписано, как ставить YARA через менеджер пакетов PIP).
YARA весьма популярна в вирлабах и фирмах, работающих по теме информационной безопасности, изучение YARA даёт весомый плюс при трудойстройстве на эти вакансии.
Специально для форума xss.pro
В данной статье мы рассмотрим популярный в последнее время антивирусный движок YARA и скриптовый язык YARA, применяющийся для описания сигнатур детектируемых объектов.
Будет подробно и пошагово рассмотрен процесс сборки приложения, которое, используя YARA-движок, будет проверять файлы и процессы по заданным нами правилам, написанным на языке YARA.
Приложение мы будем собирать максимально компактным и быстрым, с отключенным CRT и зависимостями. А также почищенным от конфиденциальной информации, которую туда добавляет компилятор от майкрософт.
Так же будет рассмотрена работа с YARA-движком на Питоне.
Подробно сконцентрируемся на первостепенных практических вопросах: как собрать EXE с YARA-движком, написать "Hello World" на YARA и тому подобных.
Так же можно эту статью рассматривать как методичку для быстрого старта и ознакомления с технологией YARA.
Основы работы с антивирусным движком YARA и его практическое применение.
До появления антивирусных движков каждый антивирус имел свой определённый набор баз со своим проприетарным форматом и свои собственные реализации процедур поиска сигнатур в файлах.
Причём всё это могло отличаться не только в разных антивирусах, но и в разных релизах одного и того же антивируса.
Постоянно вносимые изменения в основные алгоритмы работы антивирусов и полное отсутствие стандартизации отрицательно сказывалось на качестве антивирусного программного обеспечения.
Кроме того, подобный бардак в алгоритмах и базах сигнатур делал невозможным интеграцию антивирусов с другим программным обеспечением, например с IDS и EDR системами.
Поэтому компанией VirusTotal (да, именно та компания, которая предоставляет широко известный чекер файлов) был разработан единый стандарт работы антивирусного программного обеспечения, и называется он YARA.
YARA представляет собой специализированный язык, описывающий типовой набор средств и операций для поиска и детектирования вредоносного программного обеспечения.
На каждый детектируемый вредоносный объект составляется YARA-правило, содержащее в себе такие признаки детектирования, как статические и динамические сигнатуры, контрольные суммы, энтропию и много другое.
В результате этого для того чтобы сделать детектирование определённого вредоносного файла отпадает необходимость писать программный код. Всё, что нужно для детекта, можно описать с помощью простого YARA-скрипта.
YARA движок создан для того чтобы маскимально упростить детектирование вредоносных объектов, а так же стандартизировать работу программ, предназначенных для обеспечения информационной безопасности (антивирусов, IDS и EDR систем)
YARA движок позволяет сканировать как определённый файл, так и определённый участок памяти.
Функция скана памяти даёт возможность детектировать криптованные файлы - будучи распакованными в памяти они становятся не защищёнными криптом, и YARA движок их детектит ровно так же, словно и никакого крипта не делалось.
Таким функционалом, например, обладает NOD32. Из этой статьи будет понятно, как этот антивирус делает детект файлов в памяти и почему ему не страшен стандартный крипт, как RunPE так и LoadPE.
Наверное многие с такой проблемой сталкивались - криптуем файл, в статике ни одного детекта, в том числе и от нода. Но после запуска файла Nod определяет криптованный файл <b>ровно под тем же именем, что и до крипта</b>
Как будет далее видно из данной статьи, YARA движок может использоваться не только в антивирусном программно обеспечении.
Будучи очень компактным и быстрым, и написанным на C++, он легко интегрируется с любым другим программным обеспечением.
Например, с помощью YARA-движка мы можем автоматизировать поиск файлов на компе, содержащих строки "password" или "login".
Или поиск определённых последовательностей цифр, например таких как номера CC, состоящие из 16 цифр в определённом формате, построенному по алгоритму Luhn.
Учитывая возможность YARA-движка сканировать определённую область памяти мы можем сделать обход всех запущенных процессов на компе с последующим их открытием и доступом к используемой ими оперативной памяти.
И далее, используя YARA-правила, искать в памяти этих процессов нужные нам данные.
Например, была когда-то распространена POS-малварь (AlinaPOS, DexterPOS и т.д.), искавшая в памяти процессов все последовательности цифр, похожие на номера кредитных карт, сверяя их по алгоритму Luhn.
К данной малвари очень был бы актуален подключенный YARA движок, с его возможностями сканировать определённый участок памяти, применяя к нему то или другое YARA правило.
Так же существуют довольно продвинутые стилеры, которые тянут инфу не с парольных кешей или с баз SQLite браузеров, а непосредственно из областей памяти, используемой запущенными процессами данных браузеров.
Тут так же будет актуален YARA движок. С помощью него можно создать простой и эффективный скрипт, описывающий что именно нужно искать в памяти процессов браузеров.
Кроме этого всего, с помощью YARA движка можно реализовать мощный инструмент для анализа памяти компьютера.
Используя огромные возможности YARA-движка по поиску определённых паттернов, сигнатур и строк можно хорошо автоматизировать поиск и анализ нужных данных в оперативной памяти компьютера.
Делаем простейшее приложение на C++, использующее YARA движок
Для начала сделаем простейшую программу, ищущую с помощью YARA движка определённую текстовую строку в файле, путь к которому передаётся через командную строку.
Эта текстовая строка будет задана отдельным YARA правилом, которое передаётся YARA движку.
Данная программа должна быть максимально компактной (размер EXE килобайт примерно 100), не должна иметь DLL-зависимостей, должна работать как stand-alone exe
Кроме этого, в дальнейшем, мы переведём этот EXE в формат небольшой компактной DLL. Так как сейчас более популярен этот формат исполняемых файлов.
Сначала мы сделаем вариант с хранением YARA-правил в отдельных файлах на диске, в файлах с расширением *.yara
И далее переделаем в бесфайловый вариант, с хранением YARA-правил внутри EXE/DLL и считыванием этих правил YARA движком из памяти, без использования файлов.
YARA поставляется в трёх вариантах: в виде исходного кода, в виде DLL файлов и в виде статической библиотеки.
Нас интересует только статическая библиотека - именно этот вариант позволяет разместить YARA движок полностью внутри исполняемого EXE файла.
Вариант с DLL нам неинтересен и сразу же отбрасывается - нельзя с собой таскать дополнительные DLL файлы.
DLL (сокращение от Dynamic Link Library) это динамический вариант библиотек, что подразумевает под собой возможность замены или изменения библиотеки, например по причине обновления версии библиотеки.
Поэтому динамический вариант библиотек это всегда отдельный дополнительный файл. Статический вариант не предполагает в дальнейшем какого-то изменения или замены библиотеки, поэтому библиотека вшита внутрь исполняемого файла, и никакого дополнительного файла на диске не требуется (это удобный для нас вариант).
Кратко о статических и динамических библиотеках можно сказать так: динамическая библиотека это возможность замены или обновления библиотеки, статическая это быстрота, компактность и отсутствие дополнительных файлов.
Статические библиотеки для Visual Studio, которые прилинковываются напрямую к исполняемому файлу без использования DLL, имеют расширение *.LIB.
В нашем случае статическая библиотека из комплекта поставки YARA будет там лежать под именем libyara32.lib, собственно это один из двух файлов оттуда которые нас интересует для текущей задачи.
Второй файл это файл yara.h, который содержит в себе прототипы функций, вызываемых из библиотеки libyara32.lib
Больше никаких файлов из комплекта поставки YARA нам не понадобятся. Только вот эти два - libyara32.lib и yara.h
Эти два файла можно найти в приаттаченном к данной статье демонстрационном исходнике, иллюстрирующим работу с YARA.
Подключать libyara32.lib лучше не в настройках проекта, а директивой #pragma, так удобней:
Код:
#pragma comment(lib, "libyara32.lib")
Это лучше чем прописывать библиотеку где-то в настройках проекта. Прописав таким способом библиотеки где-то вначале исходника они всегда будут на виду.
И чтобы поменять что-то в подключаемых библиотеках не нужно будет где-то лазить в меню проекта и искать нужные вкладки.
Прописав подключаемые библиотеки в исходнике с помощью директив препроцессора #pragma, мы всегда будет держать их на виду, централизованно и в одном месте.
Далее прописываем *.h файл с прототипами вызова функций YARA
Код:
#include "yara.h"
Всё, с этого момента мы готовы к работе с библиотекой YARA из среды C++.
Чтобы запустить и проинициализировать YARA-библиотеку мы сначала вызываем функции yr_init и yr_create_context
Код:
YARA_CONTEXT* context = {0};
yr_init();
context = yr_create_context();
if (context == NULL)
{
puts("Context error");
exit(0);
}
В случае успешного завершения операции вернётся текущий контекст, либо нулевое значение в случае ошибки.
Контекст нужен для многопоточной работы, для каждого отдельного потока он будет свой.
Этот контекст передаётся параметром во все функции библиотеки YARA, для того чтобы YARA движок мог обеспечить многопоточную работу, это механизм межпоточного взаимодействия.
По этому контексту YARA определяет, из какого потока произошёл вызов той или иной процедуры, и какому потоку отдать данные, которые обработала та или иная функция.
Контекст представляет собой структуру, в которую можно писать настройки YARA движка, применительно для того потока которому этот контекст соответствует.
Там пишутся такие настройки, как список файлов на проверку, список YARA-правил, коды ошибок и т.д.
После завершения работы с YARA движком мы должны удалить созданный контекст, воспользовавшись функцией yr_destroy_context
Функции передаётся единственный параметр - указатель на контекст, с которым мы завершаем работу.
Код:
yr_destroy_context(context);
Мы пропишем в контекст одну из главных вещей - обработчик ошибок в YARA-правилах.
Прописывается обработчик ошибок таким образом:
Код:
context->error_report_function = report_error;
Где report_error это callback-функция следующего вида:
Код:
void report_error(const char* file_name, int line_number, const char* error_message)
{
printf("%s:%d: %s\n", file_name, line_number, error_message);
}
На входе она принимает имя файла с YARA-правилом, номер строки, где возникла ошибка, и описание возникшей ошибки.
То же самое, как и при обработке любым компилятором синтаксических ошибок в исходнике - тот так же пишет ошибку и указывает на место в файле, где эта ошибка произошла.
Это очень нужная вещь при отладке YARA-правил, без этого не обойтись - обязательно нужна обратная связь от YARA-движка о правильности синтаксиса написанных нами YARA-правил.
Основы языка YARA и пример простейшего YARA-правила для сигнатурного детекта
Рассмотрим YARA-правило для простейшего детекта, который ищёт определённую сигнатуру в файле и при нахождении этой строки YARA движок сигнализирует о детекте.
Сигнатура - это некоторая короткая, неизменная последовательность байт, присутствующая в детектируемом объекте.
Сигнатура переводится с английского как подпись (signature) - на детектируемом объекте как бы написана определённая последовательность байт, говорящая о том, что это за объект. В этом заключается смысл этого названия.
Сделаем простейший сигнатурный детект на языке YARA для файлов, запакованных пакером UPX.
Правило будет выглядеть так:
Код:
rule Packed_UPX
{
strings:
$s1 = "UPX!"
condition:
$s1
}
Сначала пишем ключевое слово rule, обозначающее начало правила.
За ним следует название правила - "Packed_UPX"
В фигурных скобках будет содержаться текст этого правила.
Правило делится на секцию данных (в данном примере это секция с именем strings) и на секцию условий (секция с именем conditions).
Имя секции заканчивается двоеточием, за именем секции на следующей строке пишется содержимое этой секции.
Секции данных могут быть различного типа, например строки, последовательности байт, хэши и т.д.
Здесь, в нашем простейшем примере, мы пишем секцию данныx типа "strings", в которой перечисляются только строковые сигнатуры.
Указываем в ней строку "UPX!", и присваиваем её переменной $s1, в этой переменной дальше и будет храниться эта сигнатура.
Строка UPX! присутвствует во всех файлах, запакованных пакером UPX, поэтому мы её и выбрали в качестве сигнатуры.
На рисунке зелёным цветом обведено место в запакованном файле, которое мы выбрали в качестве сигнатуры пакера UPX.
Эта сигнатура всегда находится в начале кодовой секции, обычно расположена первой по счёту.
Перед сигнатурой видны чиcла "3.06" - так UPX записывает номер своей версии в запакованный файл.
Далее в секции condition мы перечисляем условия, при которых срабатывает данное правило и происходит детект.
Там мы пишем условия с именем $s1 - это строка, которую мы прописали в секции strings.
Если в секции strings было указано несколько строк, то перечисляем их имена переменных через запятую.
Так же в секции condition указываются переменные, содержащие в себе последовательности байт, хеши и другие более сложные типы данных.
Если будет найден хотя бы один элемент, прописанный в секциях даннных, то правило считается сработавшим и YARA движок выдаёт детект.
Полную официальную документацию по языку YARA можно найти здесь: https://yara.readthedocs.io/en/stable/
В официальной документации будут примеры только по поиску и детектированию различных опасных объектов.
В данном цикле статей по YARA-движку и InMemory-стилерам будет много примеров по поиску и детектированию различной конфиденциальной информации в различных объектах - файлах, участках памяти и т.д.
Дальше это простейшее YARA правило подключаем к нашему тестовому проекту, демонстрирующему написание "hello world" для YARA движка.
Чтение YARA-правил из файла их компиляция и передача YARA-движку
Сохраняем только что написанное нами YARA правило в файл с именем test.yara
Для того, чтобы отправить этот файл YARA движку, пишем следующий код:
Код:
FILE* f = fopen("test.yara", "r");
if (! f)
{
puts("Error reading file test.yara\r\n");
exit(1);
}
if (yr_compile_file(f, yContext))
{
puts("Error while compiling YARA script");
}
fclose(f);
Сначала мы открываем файл "test.yara" целиком в память стандартной crt функцией fopen, которая вернёт хэндл файла,, либо ноль в случае ошибки.
Далее мы используем функцию yr_compile_file, которая на входе получает хэндл только что открытого файла, а так же хэндл контекста, полученный при инициализации YARA-движка.
В случае успешного завершения функция yr_compile_file вернёт нулевое значение, в случае ошибочного завершения вернёт код ошибки.
После вызова этой функции и компиляции YARA-правил все подготовительные действия с YARA-движком завершены.
На этом этапе YARA-движок проинициализирован, правила скомпилированы и переданы ему - движок полностью готов к детектированию объектов.
Поэтому дальше рассматриваем передачу YARA-движку проверяемого объекта и сверку его с YARA правилами.
Передача YARA-движку объекта и сверка его с YARA-правилами
Пишем дальше следующий код:
Код:
fileBuffer = readfile(argv[1]);
if (!fileBuffer)
{
printf("Error reading file %s\r\n", argv[1]);
exit(1);
}
yr_scan_mem((BYTE*)fileBuffer, _msize(fileBuffer), context, (YARACALLBACK)yara_callback, argv[1]);
Прочитываем полностью в буфер памяти с именем fileBuffer проверяемый объект (в нашем случае некий EXE файл, либо запакованный, либо не запакованный UPX).
После того как объект прочитан в память, проверяем его функцией для проверки файлов YARA-движком, это функция с именем yr_scan_mem
Первым параметром она принимает область памяти, содержащую в себе целиком проверяемый объект.
Вторым параметром идёт размер этой области памяти. Здесь мы указываем стандартную функцию _msize из stdlib, возвращающую размер выделенной памяти по указателю на неё.
Третим параметром идёт ранее рассмотренный нами хэндл контекста.
Пятым параметром идёт имя файла - YARA не будет что-то читать из этого файла или делать какие-либо другие операции с этим файлом. Это просто строка с именем файла, для генерации отчёта - файл с таким-то именем чист или есть детект.
Четвёртым параметром идёт callback-функция обработки объекта, в параметрах который передаётся результат ранее скомпилированных YARA-правил.
На этой callback-функции остановимся подробнее.
Код:
int yara_callback(RULE* rule, unsigned char* buffer, unsigned int buffer_size, void* data)
{
if (rule->flags & RULE_FLAGS_MATCH)
{
printf("Rule match %s %s\r\n", buffer, rule->identifier);
}
else
{
puts("No detect, file clean");
}
return CALLBACK_CONTINUE;
}
Данная функция обратного вызова первым параметром принимает структуру RULE, которая содержит статус обработанного правила и другую дополнительную информацию о процессе обработки данного YAКA-правила.
Из этой информации нам интересен параметр flags, по которому мы определяем применилось ли данное правило или нет. Проверяем мы этот параметр, применив логическую операцию "И" над этим параметром и константой RULE_FLAGS_MATCH (которая определена в yara.h).
Если есть совпадение правила - значит выводим сообщение о найденном детекте, в противоположном случае печатаем, что детекта нет.
В случае нахождения детекта мы печатаем имя файла, взяв его из параметра callback функции с именем buffer, и идентификатора (имени) детекта, взяв его и структуры rule.
Имя (идентификатор) детекта - это то, что мы указывали в YARA-правиле после идентификатора rule, в нашем случае это строка "Packed_UPX":
По выходу из процедуры обратного вызова могут быть два варианта.
Первый вариант это возвращение значения CALLBACK_CONTINUE. В этом случае YARA-движок осуществляет проверку следующего YARA-правила и снова вызывает callback функцию для этого правила.
Во втором случае возвращается любое другое значение, отличное от CALLBACK_CONTINUE. В этом случае дальнейшая проверка правил для данного объекта прекращается.
После завершения работы с YARA-движком и перед выходом из программы YARA-движок нужно обязательно выключить, вызвав функцию yr_destroy_context.
На первый взгляд это кажется необязательным - всё равно программа завершается, и все ранее открытые хэндлы файлов, потоки и всё остальное уничтожатся вместе с процессом, из которого мы вышли.
Но на самом деле это не так.
Часто можно встретить такую ошибку - по выходу из программы иногда появляется сообщение об ошибке, код которой либо "MEMORY ACCESS VIOLATION" либо "INVALID HANDLE".
То есть перед самым выходом где-то произошло обращение к уже освобождённой памяти или к закрытому хендлу.
И это происходит достаточно редко: раз 20 например программа завершается нормально а на 21 раз эта проблема появляется и потом надолго исчезает снова.
Эту ошибку, появляющуюся таким непредсказуемым образом, очень тяжело отловить. То ошибка есть, то её нету.
Поэтому, перед выходом из программы, нужно обязательно останавливать все запущенные потоки, закрывать все хендлы, освобождать всю память. Несмотря на то, что якобы винда должно сама всё это закрывать по завершению процесса.
Для этого, по завершению работы с YARA-движком, и нужно вызывать функцию yr_destroy_context. Это предотвратит подобные трудноотлавливаемые ошибки.
Функция yr_destroy_context корректно закроет всё, что наоткрывал и назапускал YARA-движок: закроет все хендлы, высвободит всю память, остановит все запущенные потоки и т.д.
В конечном итоге, собрав всё вышеописанныек вещи воедино, мы получаем короткую программу, которая через использование YARA-движка проверяет файл заданный в командной строке на соответствие правилу, написанному на языке YARA, и хранящемуся в файле test.yara
Этот исходник лежит в приаттаченном файле yara_hello_world.zip
Хранение YARA-правила не в отдельном файле, а внутри EXE файла.
В предыдущем примере мы сделали хранение YARA-правил в отдельном файле test.yara
Это удобно для освоения языка YARA и экспериментов с ним. В этом файле можно писать свои правила и применять его к различным файлам, задаваемым в командной строке. Осваивать элементы языка YARA: ключевые слова, структуру правил, типы данных и т.д.
Всё это удобно для изучения, тестирования и отладки.
Но хранение YARA-правил в отдельном файле хорошо только для учебно-отладочного варианта, в боевом варианте отдельный файл недопустим - всё должно храниться внутри EXE/DLL файла.
Мы всегда должны стремиться к бесфайловому режиму работы, поэтому стремимся избежать даже создания временных файлов на диске.
Рассмотрим следующий код:
Код:
char* tmpMem = readfile("test.yara");
if (! tmpMem)
{
puts("Error reading file test.yara\r\n");
exit(1);
}
yr_compile_string((char*)tmpMem, context);
free(tmpMem);
Сначала считываем целиком файл test.yara в буфер памяти tmpMem
Далее, используя функцию YARA движка yr_compile_string, компилируем это правило.
Этой функции передаётся два параметра: указатель на буфер памяти, где хранятся YARA-правила, и хэндл контекста. Этот хендл контекста мы получаем на этапе запуска и инициализации YARA-движка и мы рассмотрели его устройство в начале статьи.
Слово string (строка) в этой функции не должно смущать - здесь под строкой понимается любая последовательность печатных ASCII символов, заканчивающихся нулём.
Эта строка может быть и в мегабайт размером.
Символы переводы строки CR/LF не считаются окончанием такой строки, они являются частью этой строки.
Поэтому файл с YARA правилами, написанными построчно, всё равно считается в этом случае одной строкой.
После компиляции функцией yr_compile_string YARA-правил, буфер памяти, содержащий прочитанный файл test.yara, больше не нужен. Поэтому высвобождаем его с помощью функции free.
В этом примере мы по прежнему не ушли от использования файла test.yara, но использовали другую функция YARA-движка, которая компилирует правила не из отдельного файла, а из памяти.
Компилирует принимает указатель на строку с правилами, которая хранится где-то в памяти, а не в файле на диске.
Теперь всё, что нам остаётся сделать, это найти способ хранения данной строки с YARA-правилами внутри EXE файла.
Если yara-правило короткое, то его можно просто сохранить в виде строки:
Код:
char* yaraRule = "rule Packed_UPX\r\n
"{\r\n"
"\tstrings:\r\n
\t\r\n$s1 = "UPX!\r\n"
"\r\n"
"\tcondition:\r\n"
\t$s1\r\n"
"}\r\n"
Так это будет выглядеть внутри скомпилированного EXE:
Зелёным цветом обведено YARA-правило, то как оно выглядит внутри скомпилированного EXE файла.
За ним видны дополнительные строки, используемые в программе: диагностические сообщения, строки внутри YARA-движка и т.д.
Строка с YARA-правилом идёт первой, потому что она быда первой упомянутой строкой с исходнике.
Компилятор помещает строки в исполняемый файл в порядке их упоминания в исходнике.
Отсюда следует важный вывод - положением строк внутри исходника можно управлять. Например, мы хотим убрать строку от начала секции данных, чтобы она меньше бросалась в глаза.
Тогда в исходнике прописываем эту строку самой последней. Или прописываем в середине общего массива строк, если хотим размесить строку в середине секции данных внутри EXE.
Но если размер файла с YARA-правилами составляет, например, десятки килобайт или даже больше, то способ записи в виде строки в исходнике не подойдёт.
Поэтому YARA-правила в исходнике мы будем записывать не в виде строк, а в виде байтового массива.
В байтовый массив файлы можно переводить, используя встроенную в windows утилиту certutil
Набираем в командной строке:
Код:
certutil -encodehex test.yara test.hex
В результате выполнения данной команды мы получим файл test.hex со следующим содержимым:
Удаляем в текстовом редакторе обведённые зелёным цветом области, оставляем только область с шестнадцатеричными цифрами.
Далее редактируем эту оставшуюся область, приводим её к следующему виду
Код:
#define yaraRules_size 73
BYTE yaraRules[yaraRules_size] =
{
0x72,0x75,0x6C,0x65,0x20,0x50,0x61,0x63,
0x6B,0x65,0x64,0x5F,0x55,0x50,0x58,0x0D,
0x0A,0x7B,0x0D,0x0A,0x09,0x73,0x74,0x72,
0x69,0x6E,0x67,0x73,0x3A,0x0D,0x0A,0x09,
0x09,0x24,0x73,0x31,0x20,0x3D,0x20,0x22,
0x55,0x50,0x58,0x21,0x22,0x0D,0x0A,0x09,
0x0D,0x0A,0x09,0x63,0x6F,0x6E,0x64,0x69,
0x74,0x69,0x6F,0x6E,0x3A,0x0D,0x0A,0x09,
0x09,0x24,0x73,0x31,0x0D,0x0A,0x7D,0x0D,
0x0A};
Константа yara_size равна размеру файла, и она, так же, соответствует размеру байтового массива, в котором мы храним YARA правило.
Теперь содержимое файла test.yara находится в этом байтовом массиве.
В скомпилированном EXE файле этот байтовый массив хранится так же как и предыдущий варант со строкой.
Компилятор не делает различия в способе хранения этих данных внутри EXE - и строка из предыдущего примера и этот байтовый массив в EXE хранятся идентичным образом, и даже по одному и тому же адресу.
Таким способом внутри EXE можно хранить и любые другие файлы, не только YARA-правила как в нашем случае. Этот способ универсален.
Итак, мы избавились от необходимости хранения YARA-правил в отдельном файле на диске.
Вместо этого они теперь хранятся в секции .data внутри EXE-файла.
Передать их YARA-движку можно теперь таким образом:
Код:
yr_compile_string((char*)yaraRules, context);
Код теперь упростился всего лишь до одной строки. Тут просто передаём функции yr_compile_string указатель на байтовый массив, в котором мы поместили точную копию содержимого файла test.yara
Нестандартный вариант хранения YARA-правил (или любых других дпнных): хранение внутри DOS заглушки
Этого метода хранения данных внутри EXE/DLL файлов нет нигде в паблике.
Метод хорош тем, что позволяет уменьшить размеры EXE/DLL файла за счёт того, что в нём не будет использоваться секция хранения данных .data
Вместо станартного хранения данных в секции .data мы их будем помещать в ту область файла, где хранится DOS-заглушка.
Для начала что это вообще такое: DOS-заглушка в EXE/DLL файлах.
Открываем любой EXE/DLL файл и в самом начале мы видим следующее:
На скриншоте зелёным цветом обведён MZ (Legacy) заголовок и жёлтым цветом PE (Portable Executable) заголовок.
Белым цветом закрашена опасная область EXE файла с конфиденциальной информацией - Rich сигнатура.
Она всегда расположена между Legacy заголовком и PE-заголовком.
В конце Rich-заголовка видна его сигнатура - строка "Rich". Это сокращение от "Richard Shupak" - имя одного из разработчиков PE-формата исполняемых файлов.
Подобно тому как MZ это сокращение от имени "Mark Zbikowski" - имя разработчика формата исполняемых файлов под MS-DOS.
Как видно, в EXE/DLL файлах не один, а два заголовка. Первый заголовок это заголовок для MS-DOS, той операционной системы которая была до Windows.
Этот первый заголовок находится в самом начале файла и содержит в себе 16-битный исполняемый файл для совместимости с системой MS-DOS.
Удивительно, но этот древний архаизм есть в абсолютно любом исполняемом EXE/DLL файле. Он поддерживается и, скорее всего, далее будет поддерживаться всеми версиями Windows.
Стандартным действием этого MS-DOS файла при запуске в данной операционной системе является вывод сообщения "This program cannot be run in DOS mode." и завершение работы.
Мы же будем делать нестандартное действие.
Пишем на ассемблере следующий код:
Код:
format mz
segment zz
entry zz:entry_point
stack 128
heap 0
entry_point:
db 'rule Packed_UPX',13,10
db '{',13,10
db ' strings:',13,10
db ' $s1 = "UPX!"',13,10
db 13,10
db ' condition:',13,10
db ' $s1',13,10
db '}',13,10
db 0
Этот код скомпилируется в 16-битный EXE файл для системы MS-DOS. Он будет той DOS-заглушкой, которую мы вставим в наш EXE/DLL файл.
Первой строкой здесь идёт директива "format mz", это означает что мы компилируем исполняемый файл формата MS-DOS
Следующии 4 строки формируют стандартный заголовок MS-DOS файла.
Из них стоит обратить внимание на строку entry zz:entry_point - это определение точки входа, места откуда стартует программа.
И в этом месте мы пишем не код программы, а размещаем свои данные !
После метки entry_point мы видим YARA-правило, которое введено на ассемблере с помощью директив резервирования байтов DB (Define Byte).
Компилируем этот исходник следующий командой:
Код:
fasm dos_stub.asm dos_stub.bin
Получившийся файл dos_stub.bin будет выглядеть так:
Вначале идёт 16 битный Legacy заголовок, сразу за ним хранятся наши данные.
Область начала хранения наших данных начинается со смещения 0x20 (обведено зелёным цветом).
В нём мы видим наше YARA-правило, хранящееся по смещению 0x20 от начала файла.
Теперь подключаем эту DOS-заглушку к нашему EXE/DLL файлу с YARA-движком.
В Visual Studio открываем меню опции проекта, открываем дополнительные параметры линкера и пишем там строку: "/STUB:dos_stub.bin"
И кладём файл dos_stub.bin в корневую папку проекта:
Компилируем файл и открываем его в HEX-редакторе, видим там следующее:
Наши данные теперь хранятся в самом начале файла, а не стандартным способом в отдельной секции с именем .data !
Но на скриншоте мы видим один неприятный момент (обведено красным цветом) - по смещению 0x3C записаны какие-то четыре непонятных байта, что повреждает наши данные.
Эти четыре байта (F0 00 00 00) это записанное в обратном порядке число 0x000000F0 (числа в PE заголовках).
Число записано в заголовке в обратном порядке потому, что таков стандарт записи данных в x86 процессоре. Числа пишутся от младщего разряда к старшему.
Отсюда и название этого стандарта хранения чисел little-endian, что дословно переводится как отсчёт с малого конца.
Поэтому в этих четырёх байтах записано число 0xF0, отсчитываем от начала файла 0xF0 байт и видим что там расположен PE-заголовок.
Значит, по этому адресу линкер прописывает смещение PE-заголовка в файле (по адресу 0x3C от начала файла).
Мы это должны учитывать, поэтому будем размещать данные в DOS заглушке начиная со смещения 0x40. Сразу же за тем местом, где хранится указатель на PE-заголовок. Так мы избежим порчи наших данных линкером.
В DOS-заглушке наши данные хранятся по смещению 0x20 от начала файла. Безопасное смещение, где данные точно не перезапишутся, начинается с адреса 0x40
Поэтому первые 0x20 байт в точке входа DOS-заглушки мы заполним нулями, в результате этого YARA-правило расположится в EXE/DLL файле ровно по смещению 0x40 от начала файла.
Исходник DOS-заглушки теперь будет выглядеть так:
Код:
format mz
segment zz
entry zz:entry_point
stack 128
heap 0
entry_point:
times 0x20 db 0
db 'rule Packed_UPX',13,10
db '{',13,10
db ' strings:',13,10
db ' $s1 = "UPX!"',13,10
db 13,10
db ' condition:',13,10
db ' $s1',13,10
db '}',13,10
db 0
Единственное отличие от предыдущего варианта это то что мы поставили директиву "times 0x20 db 0" после метки entry_point
Эта директива резервирует в файле 0x20 нулевых байт. Так мы отодвинули повреждающуюся область от наших данных.
Компилируем эту DOS-заглушку, подключаем её к нашему EXE с YARA-движком, и компилируем его.
Открываем полуившийся EXE в HEX-редакторе и видим следующее:
На скриншоте красным цветом обведена перезаписываемая область (четыре байта) и зелёным цветом обведено новое смещение начала хранения данных.
Белым прямоугольником закрашена Rich-сигнатура EXE, содержащая в себе конфиденуиальную информацию о системе, на которой был скомпилирован файл.
Наше YARA-правило записалось теперь без повреждений.
Как получить теперь доступ к этим данным изнутри запущенного EXE файла ?
Первое что нужно сделать, это получить адрес памяти, по которому загружен EXE файл. То есть получить значение ImageBase.
Делается это с помощью вызова винапи GetModuleHandle(0)
GetModuleHandle(0) вернёт указатель на начало EXE файла в памяти, к нему мы добавляем значение 0x40. Добавляем значение смещения в файле, по которому лежат наши данные в DOS-заглушки.
В памяти оно будет ровно таким же, как и в файле.
В конечном итоге, указатель на данные из DOS-заглушки на C++ будет выглядеть вот так:
Код:
char* yaraRuleInDosStub = (char*)GetModuleHandle(0) + 0x40;
И далее передаём этот указатель функции yr_compile_string
Да, обращение к данным лежащим в DOS-заглушке будет крайне неудобным, но за счёт этого достигается уменьшение размера EXE-файла.
Этот способ имеет смысл, если у нас есть какие-то крупные данные, обозначенные всего лишь одной переменной.
Например, набор YARA-правил в десятки килобайт размером или какой-то дроппер в виде исполняемого файла.
Если у нас данные разбиты на множество мелких переменных, то их хранение в DOS-заглушке будет неудобным.
Например, набор из множества строк, в которых хранятся сообщения об ошибках - в DOS-заглушке такое хранить неудобно, для каждой строки надо будет искать смещение от начала файла.
И если мы изменим одну строку, то тогда изменятся смещения всех строк, расположенных сразу же за ней. И для этих строк нужно будет определять новые значения.
Хранение YARA-правил в ресурсах EXE/DLL
Ресурсы в EXE/DLL файлах были специально придуманы для того, чтобы была возможность обновить некие данные, используемые программой, не затрагивая самого программного кода.
Например, в программе все сообщения выводятся на английском языке, нужно их заменить на немецкий - как сделать замену всех строк с сообщениями ?
Для этого нужно организовать некое централизованное хранилище этих строк, и нужен некий программный интерфейс, который выдаёт программе доступ к каждой строке с сообщением.
В этом случае мы можем централизованно заменить все строки, и не заботится о том что строке будут разной длины, из-за чего изменятся указатели на эти строки в программе.
Программный интерфейс, работающий с этим централизованный хранилищем, возьмёт на себя работу по правильному размещению строк в хранилище, выдаче программе их адресов и многое другое.
Этот програмный интерфейс встроен в систему Windows и называется <B>ресурсами исполняемых файлов</B>.
Ресурсы хранятся в EXE/DLL файле в отдельной секции, обычно с именем .rsrc
Туда часто размещают строки, графику, иконки, мультимедию и т.д. То есть всё то, что предполагается к периодической замене в программе. Но при этом замену нужно сделать без внесения каких-либо правок в программный код и перекомпиляции EXE.
Например, нам нужно заменить логотип в нашей программе. Логотип хранится в виде BMP файла в ресурсах EXE/DLL файла, считывается оттуда и отображется при старте программы.
Редактором ресурсов заменяем этот логотип на другой - такая замена происходит никак на затрагивая программный код.
Наборы YARA-првавил это постоянно меняющаяся вещь. Постоянно добавляются какие-то новые объекты, обновляются правила для старых объектов, удаляются объекты ставшими неактуальными для детектирования.
Такие обновления YARA-правил могут делать по-несколько раз в сутки.
Поэтому каждый раз вносить изменения в программный код и перекомпилировать EXE это плохой вариант, так как со времнем гарантированно добавит ошибок в программном коде.
В то же время мы не хотим хранить YARA-правила в отдельных файлах, нам нужно чтобы был stand-alone exe, чтобы всё хранилось внутри одного EXE файла.
Поэтому здесь будет очень актуальным использовать механизм ресурсов, и хранить YARA-правила в секции ресурсов внутри EXE/DLL файла.
И как только мы хотим сделать обновление YARA-правил мы добавляем их в EXE с помощью редактора ресурсов, без перекомпиляции исполняемого файла и вмешательства в программный код.
Чтобы добавить в ресурсы EXE/DLL набор правил, содержащийся в файле yara_rules_list.yara, подключаем в Visual Studio *.rc файл. Это стандартный файл с описанием подключаемых к EXE ресурсов.
Редактируем его так как показано на скриншоте:
В первую очередь, на скриншоте красным цветом обведён один очень важный момент.
Визуал студия по умолчанию прописывает в ресурсах идентификатор той локали, которая установлена в данный момент на компе !
И в основном по этой записи и спаливается страна происхождения файла. В этом месте можно прописать нужную локаль, например USA.
Зелёным цветом здесь обведена запись, которую нужно добавить.
Вот этот код отдельно:
Код:
STRINGTABLE
BEGIN
IDS_YARA_RULES "rule Packed_UPX\r\n\
{\r\n\
\tstrings:\r\n\
\t\t$s1 = ""UPX!""\r\n\
\r\n\
\tcondition:\r\n\
\t\t$s1 \r\n\
}\r\n"
END
Тип ресурса STRING выбран потому, что YARA-правила представляются в ресурсах как одна большая ASCIIZ строка.
Наше тестовое YARA-правило в *.rc файле записываем следующим образом:
1. Строка пишется внутри кавычек сразу после идентификатора
2. Символы перевода строк записываем в конце каждой строоки как \r\n
3. Для удобства перенос строк в *.rc файле делается символом "\"
4. Символ табулюяции прописывается как \t
Компилируем EXE, открываем получившийся файл и находим там секцию ресурсов. Видим там следующее:
Строка с YARA-правилом, записанная в секции ресурсов, обведена зелёным цветом.
А так же на скриншоте хорошо видно то, какой огромный объём избыточной инофрмации добавляется в скомпилированные PE файлы - все те области забитые нулями не используются системой.
В них можно размещать свои данные и это никак не отразиться на работоспособности EXE - это дополнительный задел для оптимизации скомпилированных EXE/DLL файлов.
Так же мы видим, что строка записывается в ресурсы в формате Wide Char (то есть по два символа на байт, второй байт в данном случае всегда ноль).
Чтобы получить эту строку в первоначалном ASCII формате, мы получаем её с помощью апи LoadStringA. Если мы хоти получить её в том формате как она сейчас хранится в ресурсах, тогда это делаем с помощью апи LoadStringW.
Чтобы получить эти YARA-правила из ресурсов пишем следующий код:
Код:
HRSRC hRsrc = FindResourceEx(
0,
RT_STRING,
MAKEINTRESOURCE( (IDS_YARA_RULES >> 4) + 1),
MAKELANGID(LANG_NEUTRAL, SUBLANG_NEUTRAL));
int yaraStringLength = SizeofResource(GetModuleHandle(0), hRsrc);
char* yaraRulesList = (char*) malloc(yaraStringLength);
int len = LoadString(0, IDS_YARA_RULES, yaraRulesList, yaraStringLength);
if (len)
{
MessageBox(0, yaraRulesList, "YARA Test", 0);
}
free(yaraRulesList);
Первым делом нам нужно определить размер строки с YARA-правилом, хранящимся в ресурсах. Потому что нам нужно выделить память под строку, и необходимо знать количество памяти, которое нужно выделить.
Сначала находим ресурс со строкой по его идентификатору IDS_YARA_RULES с помощью апи FindResourceEx
В вызове этой апи всё очевидно, кроме указания идентификатора строки: (IDS_YARA_RULES >> 4) + 1
Дело в том, что строки ресурсах хранятся в блоках по 16 записей размером. Система считывает и записывает в ресурсы EXE строки не по одной, а пакетами по 16 строк в каждом.
Чтобы идентифицировать отдельную строку внутри каждого блока мы должны воспользоваться вышеприведённой формулой.
В этой формуле мы берём идентификатор строкового хранилища IDS_YARA_RULES и делим его на 16 ("IDS_YARA_RULES >> 4" - эта то же деление на 16, только более оптимизированная её запись).
Затем прибавляем к получившимуся результату единицу, указатель на первую запись в блоке. Там и будет храниться наша строка с YARA-правилом.
Ещё один важный момент с функцией LoadString. В интернете полно примеров когда LoadString возвращает длину строки, если ей указать последним параметром ноль.
Этот способ больше не работает начиная с WIndows10, и в MSDN написано что значение всегда должно быть отличающееся от нуля !
Под Windows10 LoadString вернёт в этом случае не длину, а нулевое значение (то есть ошибку).
Оптимизируем сборку EXE файла, получаем сверхбыстрый и сверхкомпактный антивирус в 120 Kb размером
Теперь разберёмся, насколько можно уменьшить размер только что собранного нами тестового примера, удалив из EXE всё лишнее.
Во-первых, мы можем отбросить прилинковываемую статическую библиотеку C++ stdlib, и заменит её на стандартную библиотеку msvcrt.dll из поставки windows.
Делается это так: сначала в настройках указываем "использовать динамическую DLL".
Скомпилировав EXE файл, мы сразу же добъёмся резкого уменьшения размера, так как вся RTL будет убрана из EXE и вместо этого теперь будет хранится в отдельной DLL.
Но тут есть подвох. Если мы запустим этот EXE на компе с установленной Visual Studio, то всё нормально сработает.
Но на компе, где Visual Studio нет, будет вот такой результат:
То есть система не может найти DLL, которая прописывается в систему при установке Visual Studio (MSVCR71.dll)
Как это исправлять ?
Открываем наш EXE файл в hex-редакторе и идём в таблицу импорта, в нём мы видим строку с именем этой проблемной DLL
И вместо этой строки пишем там вот это: msvcrt.dll
Лишние символы, если будут, просто забиваем нулями
В результате таблица ипорта будет выглядеть теперь так:
Пробуем запустить отредактированный таким способом EXE на компьютере без Visual Studio - всё теперь запускается.
Вместо DLL стандартной библиотеки, для какой-то определённой версии Visual Studio, мы указали универсальную стандартную библиотеку msvcrt.dll, которая есть во всех версиях Windows
В нашем случае эта библиотека не будет ничем отличаться от той библиотеки, которую прописала в EXE визуал студия.
Базовый набор функций в этих библиотеках полностью идентичен, а какие-то специфические новые функции, которые есть в новых библиотеках crt, мы использовать не планируем.
И поэтому смело прописываем в EXE универсальную CRT библиотеку Windows.
В результате, после всего этого редактирования, получаем очень компактный EXE размером всего лишь 118784 байт.
В этом очень небольшом размере получилось разместить полноценный антивирус, работающий на YARA-движке !
Оптимизируем компиляцию EXE дальше, а так же удаляем конфиденциальную информацию о компьютере, которую компилятор помещает в EXE
Первое что нужно удалить из боевой версии EXE это отладочную информацию. Кроме того, что эта информация облегчает анализ файла, она ещё хранит в себе разную конфиденциальную информацию.
Открываем EXE в hex-редакторе и набираем в поиске сроку "RSDS", откроется следующая область:
Здесь мы видим, что после строки RSDS идут какие-то загадочные 16 байт, и сразу за ними строка с путём к PDB файлу. Обычно это идентично той папке, в которой лежит проект.
Это на самом деле очень мощная подстава от Visual Studio, например в том случае если проект расположен в пользовательской папке (а по умолчания Visual Studio предлагает сохранять проекты именно там).
Таким способом, юзернейм попадает внутрь EXE файла.
Ещё хуже дело обстоит с 16 байтами, которые идут после строки RSDS.
Эти байты представляют собой GUID, которых как известно, нунжо опасаться по той причине, что они могу генерироваться на основе MAC-адреса сетевой карты.
Подробнее об этой проблеме здесь: https://www.computerworld.com/article/2541891/faq--for-microsoft--antipiracy-is-a-naked-baby.html
Там про то, как майкрософт прятал в гуидах внутри документов офиса MAC адреса. Для того чтобы выявлять пиратские копии.
Поэтому, отладочной информации в EXE файле быть не должно.
Открываем свойства проекта, и выбираем "Generate debug info: No"
С отладочной информацией внутри EXE разобрались, смотрим дальше.
Открываем внутри HEX редактора сведения о PE-заголовке. В HIEW, например, это делается нажатием клавиши F8
Видим следующее:
Первое что мы там видим это дата компиляции EXE, что весьма неконфиденциально (обведено зелёным цветом).
А так же мы там видим версию линкера, которым собирался EXE и который установлен у нас на компе (обведено жёлтым цветом).
Нам нужно удалить эту информацию из PE-заголовка, делается это следующим образом.
Переходим к началу заголовка, и отсчитываем от него 8 байт:
В этом месте PE-заголовка хранится timestamp (обведено зелёным цветом), то есть время и дата компиляции EXE.
Забиваем эти 4 байта нулями и открываем снова просмотр заголовка EXE:
Мы видим, что дата компиляции сменилась на 1 января 1970 года.
Эта дата в PE-заголовке является начальной точкой отсчёта времени, а timestamp представляет собой 32 битной число, содержащее в себе количество секунд, прошедшее с этой даты.
Много ли это или мало, и когда этих четырёх байт перестанет хватать для записи количества прошедших секунд ?
Считаем: одно 32 битное число равняется 2^32=4294967296
Делим это на количество секунд в сутках: 4294967296 секунд / 86400 секунд в сутках = 49710 суток
Делим количество суток на 365: 49170 суток / 365 суток в году = 136 лет
Отсюда получается, что как минимум до 2106 года время в заголовках PE-файлов будет обрабатываться корректно.
Дальше удаляем версию линкера: идём так же к началу PE-заголовка отсчитываем 26 байт и видим там следующее:
Видим два байта 07 и 0A (обведено зелёным цветом) что соответствует десятичным числам 7 и 10 и, соответсвенно, версии линкера 7.10
Так же затираем нулями и снова открываем просмотр заголовка PE:
Видим, что дата сборки EXE и версия линкера теперь удалены, PE заголовок почищен от конфиденциальной информации.
Теперь перемещаемся в самое начало файла и находим там так называемую Rich-сигнатуру.
Эта та область, которая на всех предыдущих скриншотах постоянно перекрывалась белым цветом.
Последовательность байт со строкой "Rich" в самом конце, расположенная между MZ и PE заголовками.
Там мы видим некую зашифрованную обдасть данных. В ней просто изобилие конфиденциальной информации: версия студии, компилятора, серийный номер, какие-то гуиды т .д.
Эту Rich-сигнатуру всегда смело забиваем нулями. На работоспособности EXE это вообще никак не отразится. Ничего, кроме очевидных проблем, эта запись в EXE файле не приносит.
Зелёным цветом здесь обведена удалённая (забитая нулями) Rich-сигнатура. На этом месте, кстати, можно разместить какие-то свои данные.
Например, под RSA-ключ там места вполне хватит, если записывать его в бинарном виде.
Так же стоит сказать об именах секций в PE-заголовке.
Смотрим на этот скриншот:
Зелёным цветом здесь обведены имена секций .text, .rdata, .data, .rsrc
Если эти имена секций забить нулями, то это вообще никак не отобразиться на запуске самого EXE.
Под каждое такое имя секции в PE заголовке отведено 8 байт, таким образом для четырёх секций в данном PE-файле зарезервировано 32 байта, в которые можно записывать что угодно, и мы можем там хранить какие-то свои данные.
Приплюсуем к этому четыре байта таймстемпа и два байта версии линкера, и получаем, что в данном PE-заголовке мы можем сохранить 32+4+2=38 байт какой-то своей информации.
Как уже здесь упоминалось, в EXE/DLL файлах содержится очень много избыточной информации. Они сильно разряжены и там встречается много пустого места, заполненного нулями.
А также там есть много областей, игнорируемых загрузчиком Windows. Туда можно записать какую-то свою информацию и на работоспособности файла это никак не отразится.
Даже в самом PE-заголовке в данном примере нашлось целых 38 байт свободного места.
В приаттаченом к статье файле getrich.cpp находится исходник дампера Rich-сигнатур.
Исхоник не мой, взят вроде бы из исходников утилиты moloch, но точно уже не помню откуда.
С помощью него можно ради интереса попроверять разные EXE/DLL файлы - в каких-то будет много содержаться конфиденциально инфы, в каких-то меньше.
И после этого появится представление о том, что будет если эту сигнатуру не удалять из своих EXE файлов.
Проблема с удалением Rich-сигнатуры может быть в том, что файлы без неё могут считаться потенциально опасными объектами.
Rich-сигнатуру, например, очень любит проверять Avira. Причём она не просто проверяет факт её наличия, но и корректнсоть и правильный её формат. Если Rich сигнатуры нет, авира, как правило, выдаёт детект типа HEUR/Generic.
Поэтому существует более сложный, но более правильный путь, чем просто забивание Rich-сигнатуры нулями - это подделка Rich-сигнатуры. Но это уже тема отдельной статьи.
Так же в языке YARA встроена возможность анализа и парсинга Rich-сигнатур.
Парсер Rich-сигнатур находится в модуле с именем "pe", а доступ к расшифрованной Rich-сигнатуре записывается в YARA одной строкой: pe.rich_signature.clear_data
Кроме того, парсер Rich сигнатур возвращает такие данные как длину, ключ, версию Rich-сигнатуры и многое другое.
Самый полезная функция это, конечно же, clear_data - она возвратит расшифрованную Rich-сигнатуру и в ней уже можно организовать побайтовый или построчный поиск.
Для тех, кто уже давно работает в сфере MALDEV, все эти неприятные вещи с Visual Studio давно уже известны.
Но если читаете впервые про то, что в EXEшники попадает какая-то конфиденциальная инфа при их сборке, то это всё будет экстремально важной и полезной информацией.
Чистить всё это вручную не так уж и сложно, но можно и написать каких-то простых скриптов на питоне, автоматизирующих все вышенаписанные действия.
Я предпочитаю чистить вручную - а вдруг в скрипте были какие-то ошибки и в результате что-то там не почистилось, лучше не лениться и проделать все эти операции вручную.
Тем более, что это не так уж много времени занимает.
Можно обойтись и без редактирования EXE, отведя под сборку EXE-файлов отдельный комп, на котором точно нет ничего конфиденциального и он всегда отключен от сети.
Отдельный комп неудобен, но хорош по той причине, что мы не можем точно знать - а есть ли ещё какие-то другие способы утечки конфиденциальных данных через сборку EXE ? Что ещё, куда, и каким способом визуал студия может помещать в бинарник ?
А ещё лучше, собирать на таком отдельном компе, с последующим удалением вручную всех вышеперечисленных проблемных мест.
Для компиляции используем всегда старые версии студий. Тут есть два проверенных варианта: VS2003/2005 и VS2010
Первая умеет делать очень компактные 32 битные EXE и мало добавляет метаинформации в скомпилированные файлы.
Вторая умеет собирать 64 битные EXE, но 32 битные EXE получаются крупнее и уже больше добавления метаинформации.
Поэтому. если EXE 32 битный (это будет в большинстве случаев) предпочтительнее использовать VS2003/2005, если предполагается делать 64 битный то тогда VS2010
Если нам нужно добавить то, чего нету в старых версиях визуал студии, то просто подключаем к ним библиотеку BOOST. В бусте есть абсолютно всё.
Например, в VS2003 нету поддержки регулярных выражений - тогда ставим boost и используем "<boost/regex>".
Ещё ряд дополнитеьных способов, как "утрамбовать" EXE.
Можно объединить секцию с программным кодом и секцию данных - так будет ещё дополнительный выигрыш в размере.
Ставим минимильное выравнивание секций (параметр FileAlign) - выигрываем ещё пару килобайт размера.
Так же, кроме стандартной библиотеки, мы можем выбросить стандартный код C++ для инициализации программы. Указав вместо стандартной точки входа main свою собственную.
В этом случае пропадёт доступ к аргументам командной строки через переменные argv/argc, но доступ к командной строке всё равно останется, например через апи GetCommandLina(A|W)
Сделать всё это можно добавив в исходник следующую строку:
Код:
#pragma comment(linker, "/MERGE:.rdata=.data /MERGE:.data=.text /SECTION:.text,ERW /FILEALIGN:512 /ENTRY:entryPoint")
Этой строкой мы указываем дополнительные опции компилятору.
Команда /MERGE объединяет три секции ".rdata", ".data" и ".text" в одно целое - теперь и данные и код будут лежать в одной секции с именем .text
По умолчанию на секции .text стоят только атрибуты RW, это означает что запись в область памяти этой секции запрещена, а это создаёт нам неудобства.
Поэтому, командой /SECTION указываем для секции .text аттрибуты доступа ERW (Execute,Read,Write), что означает полный доступ к памяти на выполнение, чтение и запись.
Параметром /FILEALIGN мы задаём самое минимальное значение выравнивания секций, которое может быть (512 байт, по умолчанию оно равно 4096 байтам).
И параметром /ENTRY мы указываем свою точку входа вместо стандартной точки входа main. Этим действием мы отбрасываем crt startup код, тем самым экономим место.
И финальное действие, идём во вкладку "Опции компилятора", и задаём там максимальную степень оптимизации кода.
С включенной полной оптимизацией выигрываем ещё 1-2 Kb в размере файла.
Итак, на данном этапе мы добились минимального размера EXE-файла и почистили его от содержащейся в нём конфиденциальной информации.
Теперь финальный этап: паковка EXE-файла. Сделаем это с помощью паковщика UPX и отредактируем запакованный файл так, чтобы его нельзя было распаковать обратно этим же пакером.
Набираем в командной строке:
Код:
upx -9 test_yara.exe
Параметр "-9" означает уровень степени сжатия, указываем максимальный, девятый.
После паковки файл ужмётся примерно в два раза.
Такой хороший уровень сжатия достигается из-за того, что EXE файлы содержат в себе много повторяющихся нулей и других повторяющихся последовательностей данных. А такие повторяющиеся данные хорошо сжимаются.
Обратно файл можно распаковать слеюующей командой:
Код:
upx -d test_yara.exe
Параметр "-d" означет decompress, то есть распаковку.
Сделаем так, чтобы запакованный файл нельзя было распаковать этой командой.
Открываем запакованный файл в HEX редакторе и ищем там строку "UPX!"
И просто забиваем её нулями.
Пробуем распаковать отредактированный таким способом пакованный EXE файл той же командой: upx -d test_yara.exe
И получаем следующий ответ от пакера UPX о невозможности распаковки:
Зелёным цветом здесь обведено сообщение пакера UPX о невозможности распаковки и о том что файл возможно был модифицирован.
От профессионального реверсера данное действие, конечно же, не спасёт, но как очень быстрый и простой способ защиты EXEшника вполне сгодится.
Итак, на данном этапе, мы получили очень компактный файл размером в 30 килобайт, который умеет делать детект объектов по правилам, написанным на языке YARA.
Если мы добавим рекурсивную проверку всех файлов на диске, а не только проверку файла указанного в командной строке, то мы фактически получим полноценный антивирус, размер которого будет всего-лишь 30 Kb.
И будет он сканировать файлы с огромной скоростью, за счёт того что оттуда выкинуто всё лишнее, а так же остутствует GUI, который на себя забирал бы огромную часть вычислительных ресурсов.
Антивирус с YARA-движком сделали, и смотрим дальше - а можно ли вместо антивируса сделать, например, стилер с YARA движком ?
Применение YARA-движка в MALDEV
Допустим нам необходимо найти тестовый файл с определённым содержимым, найти его либо рекурсивным обходом всех файлов на диске, или, например, отслеживая открытие любых *.txt файлов.
Например, нас интересует файл, в котором хранится список логинов и паролей.
Нам известно, что логин там начинается со строки "Login:" и пароль со строки "Password:".
Таких записей там несколько сотен, по этой причине признаком данного файла с паролями мы считаем нахождение в файле каждой из строк "Login:" и "Password:" более пяти раз - для того чтобы убрать ложные детекты.
Кроме этого, нам известно что у интересующего нас файла гарантированно большой размер, поэтому мы отсекаем все мелкие файлы. Мелкими файлами мы будем считать файлы, размер которых меньше 5 килобайт.
Пишем простое YARA-правило для отслеживания файла с перечисленными признаками:
Код:
rule fileWithPasswords
{
strings:
$loginStr = "Login:"
$passwordStr = "Password:"
condition:
#loginStr > 5 and #passwordStr > 5 and filesize > 5KB
}
Мы создали YARA-правило, при срабатывание которого YARA-движок будет выдавать детект с именем fileWithPasswords.
В секции данных strings иы перечисляем строки, наличие которых мы проверяем в файле.
В секции condition мы пишем условие для проверки количества нахождений этих строк в файле.
Для обозначения количества нахождений переменной в проверяемом объекте в YARA перед имнем переменной ставится символ "#"
В данном примере, #loginStr будет являться числом, равным количеству строк, содержащемся в проверяемом нами *.txt файле.
Проверка условия представляет собой логическое выражение, которое своим синтаксисом похоже на оператор в языке Python.
Приоритет операций (сравнение ">" выполняется первым, логическая операция and выполняется после сравнения), порядок расстановки скобок - всё то же самое, как в питоне.
Ключевое слово filesize в языке YARA представляет собой размер проверяемого файла (если проверяем область памяти, то это ключевое слово вернёт значение 0), в данном условии мы не детектируем файлы, размер которых меньше чем 5 килобайт.
Результат работы данного YARA-правила - будет выдаваться детект на все проверяемые файлы (объекты), в которых будут найдены строки "Login:" и "Password:" более 5 раз, и размер этого файла будет превышать 5 килобайт.
Оговорка про слово "объект" не случайна - объектом может быть не только файл, но и, например, определённый участок памяти, или фрагмент передаваемых по сети данных.
То есть мы можем поймать данный интересующий нас список паролей не только путем отслеживания и поиска файлов на диске, но и путём мониторинга памяти запущенных процессов либо мониторинга сетевой активности.
Данное YARA-правило отражает собой основной принцип работы целого класса защитного программного обеспечения, именуемого DLP-системами.
DLP расшифровывается как Data Leakage Protection, что переводится как предотвращение утечки данных.
В настройках DLP систем прописываются определённые правила для защищаемых объектов (очень часто на рассматриваемом нами языке YARA), после чего DLP система отслеживает все объекты, подпадающие под данные правила.
Например, DLP система отследила объект попавший под определённое YARA-правило и была зафиксирована попытка отправки данного объекта куда-то по сети - DLP система выдаёт сообщение о нарушении безопасности.
Если вместо выдачи сообщения о нарушении безопасности мы отправляем этот файл куда-то в админку на дедике, то это будет не DLP-система, а система противоположная ей - малварь.
Отсюда делаем важный вывод: принцип работы у DLP-систем и у малвари один и тот же, разница есть только в реакции на обнаруженные данные - либо их отправка с компа либо наоборот, предотвращение отправки
Это означает, что мощную DLP-систему можно использовать как мощный стилер, и любую опенсорсную DLP-систему можно переделать в гибко настраиваемый и конфигурируемый стилер.
Подключение к YARA-движку дополнительных модулей и библиотек. Модуль для анализа PE-файлов (то есть исполняемых EXE/DLL файлов).
В YARA имеется возможность подключения различных дополнительных модудей и плагинов.
В базовом наборе модулей, поставляемых с YARA-движком, сгруппированы различные наборы операций и инструментов, для анализа и детекта какого-то определённого типа объектов.
Одним из таких важнейших модулей является модуль для анализа исполняемых файлов, то есть файлов типа EXE/DLL.
Подключается он в YARA таким образом:
Код:
import pe
import time
rule ATTENTION_file_compiled_today
{
condition:
time.now() - pe.timestamp < 86400
}
Подключение дополнительных модулей в YARA делается с помощью ключевого слова "import" и следующего за ним имени модуля.
Ключевое слово.import всегда указывается вне области правила, то есть не внутри фигурных скобок и не между строкой с ключевым словом rule и фигурными скобками.
Кроме того, здесь указан модуль time, позволяющий получить текущую дату и время на компьютере.
Данное правило иллюстрирует один очень неприятный детект, с которым сталкиваются те, кто занимается криптом EXE.
В секции условия детекта в данном YARA-правиле проверяется дата и время компиляции EXE/DLL файла (именно то, что мы удаляли из EXE файла, когда чистили EXE от конфиденциальной информации, которую там оставляет линкер).
Затем, с помощью функции time.now(), мы получаем текущее время в секундах. И вычитаем из него время компиляции EXE, которое хранится в значении pe.timestamp
Затем получившуюся разницу мы сраниваем с 86400 секундами (количество секунд в одних сутках, 60 секунд*60 минут*24 часа=86400 секунд).
Если разница получилась меньше, значит детектим данный файл как скомпилированный менее суток назад. Такие файлы AV всегда держит под подозрением.
Конечно, свежесобранный файл AV не записывает сразу в малварь, но на такие файлы делается особо тщательная проверка.
И в результате вылезают те детекты, которых могло бы и не быть, если бы в заголовок PE был бы правильно скорректирован timestamp.
Профилактика такого детекта очень простая - в виртуалке, или на том отдельном компе где происходит финальная сборка, должна быть установленна дата и время, сильно отстающая от текущей. Например на год меньше текущей даты.
Это короткое YARA-правило демонстрирует всю мощь YARA-движка. Написав всего лишь 5 строчек мы задетектили огромную выборку потенциально опасных файлов.
Детектирование опасных объектов с помощью YARA или аналогичных движков сильно упрощается, в этом и заключается ответ на вопрос, почему после крипта файлы теперь начинают детектится через 2-3 часа.
Ещё один пример, детектор оверлеев в EXE/DLL файлах:
Код:
import pe
rule ATTENTION_overlay_detected
{
condition:
filesize > pe.size_of_image
}
Здесь, в секции проверки условий, мы сверяем фактический размер файла с размером файла, прописанным в PE-заголовке.
Загрузчик Windows, обрабатывая PE-файл перед запуском, считает размером EXE/DLL файла именно то значение, котоое указано в поле заголовка pe.size_of_image
То, что находится на диске за пределами этого размера, загрузчиком Windows игнорируется.
Там программа может хранить какие-то свои данные. Например SFX-архивы там хранят свои запакованные файлы. Или малварь хранит свой блок настроек.
Если есть превышение фактического размера файла на диске и его размером в памяти, считается что в конец файла записан оверлей.
Наличие оверлея всегда вызывает подозрение у AV - это очень редко встречающаяся ситуация у легетимных файлов. Из легетимных файлов с оверлеем это, в основном, различные SFX-архивы и некоторые инсталляторы.
В остальных случаях такой файл считается подозрительным и для него производится особо тщательная проверка, так же как и в только что рассмотренном случае со свежим таймстемпом в PE-заголовке.
Ещё пример, определяющий страну сборки исполняемого файла.
Код:
import pe
rule ATTENTION_exe_from_danger_country
{
condition:
pe.language(0x19)
}
Данный пример описывает детект на все EXE/DLL файлы, которые собирались в Российской Федерации.
Функция pe.language берёт идентификатор локали из секции ресурсов PE-файла. В этой секции, по умолчанию, всегда прописывается текущая локаль компа.
Она называется LCID и представляет собой шестнадцатеричное число, в двнном случае этой число 0x19, что означает Российская Федерация.
Новые версии линкеров любят прописывать по умоланию секцию ресурсов .rsrs в собираемый файл, даже в том случае, если никаких ресурсов программа использовать не будет.
Просто это секция делается с минимальным размером и пустой. Но идентификатор локали там остаётся. В результате, по этой секции можно определить страну.
Данный детект обходится так же просто - на том компе или виртуалке, где происходит финальная сборка, ставим английскую винду. Тогда локаль в ресурсах будет прописываться как 0x09 (English).
Тут вырисовывается та же проблема, что и у вбивщиков кредитных карт.
Вбивщику необходимо максимально замаскироваться под холдера: поставить английскую версию винды, выставить правильное время на компе и часовой пояс и сделать ещё кучу настроек, чтобы комп, с которого идёт вбив, максимально был похож на комп холдера.
Теперь, при финальной сборке EXE/DLL, мы оказываемся в том же положении, что и этот вбивщик.
Для чистки от детектов, для финальной сборки EXE/DLL нужен отдельный комп или виртуалка. Туда нужно поставить английскую винду, английскую версию визуал студии, откатывать системное время на компе на год назад и делать многие другие вещи, чтобы скомпилировыанные EXE были максимально похожие на легитимные.
Это спасёт от первичных детектов, от тех признаков которые AV проверяют в первую очередь. Время сборки, подозрительная страна сборки и т.д.
YARA в Kali Linux
Утилит YARA так же присутствует в дистрибутиве Kali Linux.
Набираем в командной строке "yara":
В случае если yara не установлена, система спросит, установить лия yara или нет, скачав её из репозитория.
Нажимаем "Y" и ждём завершения процесса установки:
После установки утилитой YARA пользоваться можно так, набрав в командной строке:
Код:
yara file_with_rules_list.yara file_for_test
Здесь file_with_rules_list.yara это файл с YARA-правилами, file_for_test это тестируемый файл.
Это простейший способ вызова утилиты YARA, полный список её возможностей можно получить набрав в командной строке "yara --help".
Данная утилита очень удобна для отладки YARA-правил до прописывания их в готовом продукте (EXE/DLL файле).
Отладили этой утилитой разные YARA-правила, оттестировали на нужных файлах, убедились что всё нормально работает - после этого прописываем их внутри нашего EXE/DLL.
Очень удобно с помощью данной утилиты нализировать большое количество вредоносных файлов.
Например, у нас в каталоге лежит несколько тысяч EXE файлов.
Нам, например, нужно отфильтровать оттуда лишь те, размер которых менее 10 килобайт, скомпилированных не ранее, чем неделю назад, и страна происхождения которых USA.
Для этого пишем такое YARA-правило:
Код:
import pe
import time
rule fileFilter
{
condition:
(filesize < 10KB) AND (time.now() - pe.timestamp < 86400*7) AND pe.language(0x09)
}
И сохраняем его в файл file_filter.yara
Далее запускаем утилиту YARA таким способом:
Код:
yara file_filter.yara *
Здесь символ "*" обозначает проверить все файлы в текущем каталоге.
После запуска утилита yara выдаст построчный список файлов, подходящих под прописанные в yara-правиле условия.
Использование YARA-правил в Python
Для тех, кто программирует на Python, будет хорошей новостью что для него доступен модуль YARA, подключение и настройка которого намного проще, чем в уже рассмотренном нами варианте с C++.
На Python очень удобно делать прототипирование работы с YARA движком (и вообще любое прототипирование, Питон для этого подходит просто идеально).
Собрали прототип на Питоне. Убедились что определённая задумка, выполненная в виде прототипа, вполне себе работает.
И после этого прототип, написанный на Питоне, переводим уже на любой нейтив код, например на C++.
Установка YARA стандартная, через менеджер пакетов PIP:
Код:
pip3 install yara-python
Далее можно пользоваться пакетом YARA из среды Python, пишем простейшую программу на питоне для работы с YARA-правилами:
Код:
import yara
rules = yara.compile('test_rule.yara')
matches = rules.match('testfile.exe')
print(matches)
Первым делом импортируем библиотеку c YARA движком, написав "import yara"
Второй строкой компилируем YARA-правила, содержащиеся в файле test_rule.yara с помощью функции yara_compile
Функция yara.compile возвратит объект rules, с помощью которого можно делать различные операции над только что скомпилированными правилами.
Одной из таких операций является применение правил к определённому файлу, что мы и сделали в четвёртой строке, вызвав метод match объекта rules с именем файла testfile.exe
Метод match возвращает список имён задетектенных объектов. Если детектов нет, то возвращается пустой список.
Сделаем тестовый файл testfile.exe и потестируем всё это на нём.
В текстовом редакторе набирем некоторое количество случайных букв, и где-то среди них размещаем две строки: "UPX!" и "12345"
Тестовый файл testfile.exe выглядит так:
Зелёным цветом здесь обведены места в файле, которые мы будем детектировать с помощью YARA-движка.
Будем считать, что эти две строки являются сигнатурами, и при нахождении таких строк в файле YARA должны выдавать детекты с именами Packed_UPX и Bytes_12345
Набор YARA-правил для этого случая, состоящий из двух правил, будет выглядеть так:
Код:
rule Packed_UPX
{
strings:
$s1 = "UPX!"
condition:
$s1
}
rule Bytes_12345
{
strings:
$s1 = {31 32 33 34 35}
condition:
$s1
}
В правиле Packed_UPX мы сигнатуру указали как строку.
В правиле Bytes_12345 мы строку "12345" указали в байтовой форме - это сделано для того, чтобы продемонстрировать то, как в YARA прописывается тип данных "последовательность байт".
Последовательность байт пишется в фигурных скобках, в виде набора шестнадцатеричных чисел, отделённых друг от друга пробелами.
Результат работы данного скрипта с этим YARA-правилом будет выглядеть так:
На экран выводится список из строк, элементами которого являются строки с именами детектов, то есть имена YARA-правил которые сработали на тестируемом файле.
Если мы применим данный скрипт к другому файлу, где такие строки остутствуют, то скрипт вернёт пустой список:
YARA-движок в Питоне так же умеет проверять не только файлы, но и запущенные процессы, находя их по идентификатору PID (Process ID)
Сейчас сделаем следующее: напишем YARA-правило для реального EXE-файла, и сделаем его детект в памяти, детект уже запущенного процесса.
Это так же будет хорошей демоснтрацией того, как работает NOD32 с его детектами в памяти. В NOD32, скорее всего, встроена не YARA, но очень похожая по принципу работы система.
Так же мы проделаем все действия по прописыванию детекта, которые делают работники вирлаб, когда им попадаёт объект который они должны добавить себе в базы.
Конечно же, в очень сильно упрощенном виде, но основа действий будет понятна.
Делать это мы будем на файле putty.exe
Откроем файл putty.exe в HEX-редакторе и ищём строку, которую назначим сигнатурой. Нам надо найти такую строку или последовательность байт, которая встретится только в детектируемом файле и не встретится в любом другом.
Чтобы не было ошибочного детекта.
Подходящая последовательность байт изображена на этом скриншоте:
[
На скриншоте зелёным цветом обведена последовательность байт со строкой "PuTTY" и следующим за ней нулевыми байтми. Её мы и выберем в качестве сигнатуры.
Делаем YARA правило для этой последовательности байт:
Код:
rule Putty_detected
{
strings:
$s1 = {00 00 00 00 00 00 00 00 50 75 54 54 59 00 00 00}
condition:
$s1
}
Сохряняем данное правило в файл putty_detect.yara и дальше работаем с ним через Python
Далее пишем скрипт, который задетектит в памяти <b>запущенный процесс</b> putty.exe (но не сам файл putty.exe)
Скрипт выглядит так:
Код:
import yara
rules = yara.compile('putty_detect.yara')
matches = rules.match(pid=10232)
print(matches)
Здесь всё так же, как в предыдущем скрипте для детекта файла, только вместо имени файлы мы указываем pid=некоторое_число
Где это значение PID взять ?
Получаем его так: сначала запускаем putty.exe и в командной строке вызываем следующую команду:
Код:
tasklist /FI "imagename eq putty.exe"
Команда tasklist используется для получения списка процессов и информации о каждом из них (аналог команды ps в линуксах)
Если набрать tasklist без параметров, то он выведет информацию о всех процессах, запущенных в системе.
На нужна информация только о процессе с именем putty.exe
Поэтому в параметрах команды tasklist мы указали фильтр для выводимой информации, так чтобы отображался только процесс putty.exe
Фильтр выводимой информации в команде tasklist указывается с помощью параметра "/FI" и следующего за ним в кавычках описателя фильтра.
В данном случае это строка "imagename eq putty.exe"
imagename означает тип фильтруемого элемента (в данном случае фильтрация по имени процесса).
eq означает "равно", это сокращение от слова "equal" (равенство)
И дальше, после равно, указывается имя нужного нам процесса (putty.exe)
Итак, результат работы этой команды будет выглядеть так:
Значение PID, полученное с помощью этой команды, прописываем в скрипт на Питоне.
Запускаем скрипт yara_test.py, получаем следующий результат:
Вот это всё, что мы только что проделали - так примерно и выглядит работа сотрудника антивирусной лабы, в рабочие обязанности которого входит добавление объектов в базы.
В очень сильно упрощённом виде, в реальности таких детектов с одной строкой в yara-правиле не бывает. Но принцип один и тот же.
Только что разобранный пример с детектом EXE файла на Питоне это было реальное тестовое задание при устройстве на работу в одну из AV лаб.
Задание было сформулировано так: в zip архиве лежит набор из нескольких файлов, нужно сделать детектирование этих файлов на Питоне.
Детект должен быть сделан как для файлов на диске, так и для процессов, запущенных из этих файлов.
Список рабочих инструментов такой: Питон, YARA, hex-редактор.
YARA в Питоне не установлена, установить её надо самостоятельно (поэтому здесь было расписано, как ставить YARA через менеджер пакетов PIP).
YARA весьма популярна в вирлабах и фирмах, работающих по теме информационной безопасности, изучение YARA даёт весомый плюс при трудойстройстве на эти вакансии.