Сергей Попов
Администратор
- 30.12.2015
- 6 184
- 6 946
- Специализация
- OSINT
- Веб-безопасность
- Статус верификации
- ✓ Verified
За последний квартал через пайплайн триажа одной TI-команды прошло больше 12 тысяч PE-файлов. Ручного реверса потребовали 340. Остальное отсеялось автоматически: легитимные инсталляторы, известные семейства по imphash, дубликаты. Без автоматизированного анализа PE-файлов этот объём убивает аналитика за пару недель - проверено на собственной шкуре. Ниже - конкретный pipeline, который собирается за день из открытых инструментов, работает офлайн и масштабируется от десятка семплов до потока в тысячи бинарников.
Место триажа в цепочке malware-анализа
Триаж вредоносных файлов - сортировка, а не анализ. Задача - за миллисекунды рассортировать бинарник: чистый, подозрительный, вредоносный, требует ручного разбора. В цепочке DFIR и Threat Intelligence триаж PE-бинарников стоит между получением семпла и глубоким реверсом в дизассемблере. Подробнее - в нашем подробном разборе бинарный анализ уязвимостей.Полная цепочка:
- Получение семпла - из CAPE Sandbox, EDR-детекта, почтового шлюза, ручного сбора с хоста
- Автоматизированный триаж - PE header analysis, энтропия секций, YARA, imphash-lookup
- Решение - drop (чистый), escalate (подозрительный), analyze (требует ручного реверса)
- Глубокий анализ - дизассемблер, дебаггер, декомпилятор, динамическое исполнение
Требования к окружению
Для воспроизведения описанных шагов:- ОС: Linux (Ubuntu 22.04+) или Windows 10/11 - скрипты кроссплатформенные
- Python: 3.9+
- Библиотеки:
pefile(2023.2.7+),lief(0.14+),yara-python(4.3+) - RAM: 4 ГБ минимум, 8 ГБ рекомендуется при обработке >1000 файлов за сессию
- Сеть: не требуется - весь pipeline работает offline
- Дополнительно: Detect-It-Easy (DiE) 3.09+ для визуальной верификации
PE header analysis: автоматизированный разбор заголовков
PE-заголовок - первое, что читает triage-скрипт. Из заголовка за миллисекунды вытягиваются десятки параметров, каждый из которых может быть маркером аномалии. Статический анализ PE-заголовков не требует выполнения бинарника и безопасен на аналитической станции без песочницы.DOS Header и поле e_lfanew
Значениеe_lfanew по смещению 0x3C указывает на начало PE-сигнатуры. В нормальном бинарнике оно лежит в диапазоне 0x40–0x200. Если e_lfanew > 0x400 - между DOS-заглушкой и PE-заголовком спрятан аномально большой блок данных. Малварь использует это пространство для хранения зашифрованного payload или шеллкода. Значение e_lfanew < 0x40 физически невозможно - DOS header занимает 64 байта. Такие файлы либо побиты, либо crafted для эксплуатации парсеров.Проверка через
pefile - обращение к pe.DOS_HEADER.e_lfanew. Выходит за пределы допустимого диапазона - файл получает повышенный score подозрительности.COFF File Header: маркеры аномалий
Ключевые поля для триажа бинарников:Machine -
0x14C (i386) или 0x8664 (AMD64). Экзотика вроде MIPS или ARM в десктопной малвари - маркер нестандартного компилятора или целенаправленной обфускации.TimeDateStamp - метка времени компиляции. Значение из будущего или из 1970-х - типичный признак тамперинга. Многие билдеры малвари зануляют или рандомизируют это поле, чтобы затруднить атрибуцию.
NumberOfSections - Windows PE loader исторически поддерживает до 96 секций (точный лимит зависит от версии ОС и размера SizeOfOptionalHeader). Файл с одной-двумя секциями при размере >1 МБ подозрителен: вероятна упаковка. Файл с >20 секциями - возможно артефакт обфускатора или протектора.
Characteristics - флаг
[URL='https://learn.microsoft.com/en-us/windows/win32/debug/pe-format#characteristics']IMAGE_FILE_DLL[/URL] (0x2000) в файле с расширением .exe - аномалия. Отсутствие IMAGE_FILE_EXECUTABLE_IMAGE - файл не является валидным PE для выполнения.Optional Header и точка входа
Magic -0x10B для PE32, 0x20B для PE32+. Несоответствие Magic и Machine - явная аномалия.AddressOfEntryPoint - точка входа, указывающая внутрь секции с данными (не
.text) или в последнюю секцию, - классический маркер пакера. UPX ставит entry point в секцию .UPX1. Кастомные пакеры часто лепят новую секцию в конец и перенаправляют EP туда.SizeOfImage - значение, не выровненное по
SectionAlignment, нарушает спецификацию PE-формата. Загрузчик Windows в ряде версий это проглатывает, но аналитически это маркер манипуляции.Subsystem -
IMAGE_SUBSYSTEM_WINDOWS_CUI (консольное приложение) у файла без консольного вывода может указывать на утилиту, запускаемую скрытно.Библиотека
pefile (активно поддерживается, последний релиз 2023, 1.8k+ звёзд на GitHub) даёт доступ ко всему перечисленному: pe.FILE_HEADER.TimeDateStamp возвращает Unix-timestamp, pe.OPTIONAL_HEADER.AddressOfEntryPoint - RVA точки входа. Чтобы определить, в какой секции находится EP, достаточно пройтись по pe.sections и сверить VirtualAddress + Misc_VirtualSize каждой секции с RVA.LIEF работает быстрее на больших объёмах и умеет парсить не только PE, но и ELF с Mach-O. Для чистого PE-триажа библиотеки взаимозаменяемы, но LIEF удобнее, когда нужно залезть в overlay и ресурсы.
Энтропия секций и обнаружение упаковщиков PE
Энтропия Шеннона - главный числовой индикатор упаковки или шифрования. Для каждой секции PE-файла считается значение от 0 (все байты одинаковые) до 8 (максимальная случайность - зашифрованные или сжатые данные).Пороговые значения энтропии
- < 1.0 - секция забита нулями или повторяющимися данными (
.bss, неинициализированные данные) - 4.0–6.5 - нормальный скомпилированный код (
.text) или данные (.rdata,.data) - 6.8–7.2 - серая зона: может быть сжатый ресурс (PNG, ZIP внутри PE) или упакованный код
- > 7.2 - с высокой вероятностью шифрование или сильная компрессия. UPX-packed секции дают ~7.0–7.4, кастомные пакеры с AES - 7.8–7.99
pefile метод section.get_entropy() считает энтропию Шеннона) по байтам секции. Но одна только энтропия для вердикта недостаточна - сжатые ресурсы (иконки, манифесты) тоже дают высокие значения. Энтропию надо комбинировать с анализом имени секции, её флагов и числа импортов.Имена секций и сигнатуры пакеров
Стандартные имена секций MSVC-бинарника:.text, .rdata, .data, .rsrc, .reloc. Отклонения:.UPX0,.UPX1- UPX.nsp0,.nsp1- NsPack.aspack,.adata- ASPack.themida- Themida/WinLicense- Произвольные имена из случайных символов - кастомный пакер
- Пустое имя секции (8 нулевых байт) - частый артефакт обфускаторов
diec --json sample.exe - и отдаёт структурированный JSON с детектами.Сравнение инструментов реверс-инженера для обнаружения упаковщиков
| Критерий | Detect-It-Easy | pefile + entropy | YARA PE module |
|---|---|---|---|
| База сигнатур пакеров | 2000+ сигнатур | Нет (только метрики) | Пользовательские правила |
| Скорость на 1 файл | ~200 мс | ~50 мс | ~30 мс |
| Batch-обработка | CLI с JSON-выводом | Нативно из Python | Нативно из Python |
| Кастомизация | Lua-скрипты | Полная (Python API) | Полная (YARA DSL) |
| Когда использовать | Визуальная верификация, единичный анализ | Pipeline, массовый скрининг | Pipeline, matching по паттернам |
| Когда НЕ использовать | Поток >10k файлов (медленно) | Нужен детект конкретного пакера | Нет готовых правил под задачу |
YARA-правила для автоматизированного анализа PE-файлов
YARA (поддерживается VirusTotal, стабильный релиз 4.5, 8k+ звёзд на GitHub) - индустриальный стандарт для pattern matching. Модульpe даёт доступ к структурированным данным PE-файла: заголовкам, секциям, импортам, экспортам, ресурсам и overlay.Аномальная точка входа - один из самых надёжных маркеров модификации бинарника. Правило, детектирующее EP вне первой секции:
Код:
import "pe"
rule suspicious_entrypoint {
condition:
pe.entry_point < pe.sections[0].raw_data_offset or
pe.entry_point > pe.sections[0].raw_data_offset +
pe.sections[0].raw_data_size
}
pe.entry_point в YARA - это смещение в файле, а не RVA. Правило может давать false positive, если .text не первая секция. Для более точного детекта пакеров стоит проверять попадание EP в последнюю секцию у файлов с >2 секциями.Минимальный набор импортов - ещё один маркер. Если весь Import Address Table сводится к
LoadLibraryA + GetProcAddress, бинарник разрешает остальные API динамически. Типичная картина для пакеров и малвари:
Код:
import "pe"
rule dynamic_api_resolution {
condition:
pe.number_of_imported_functions < 5 and
pe.imports("kernel32.dll", "GetProcAddress") and
pe.imports("kernel32.dll", "LoadLibraryA")
}
math в YARA позволяет считать энтропию прямо в правиле через math.entropy(offset, size), связывая её с данными из PE-модуля.Ограничения YARA в статическом триаже
YARA работает с файлом как с набором байтов и структур - она не выполняет бинарник:- Динамическая распаковка невидима для YARA
- Self-modifying код не анализируется
- Многослойная упаковка (пакер поверх пакера) детектируется только по внешнему слою
- False positive на легитимных пакованных приложениях (игры, DRM-защищённый софт)
Импорты как поведенческий отпечаток бинарника
Таблица импортов - профиль поведения программы до её запуска. По импортам без выполнения кода видно: работает ли файл с сетью, создаёт ли процессы, лезет ли в реестр, инжектит ли код в чужие процессы.Imphash и кластеризация семплов
Import hash (imphash) - MD5-хеш нормализованного списка импортированных функций. Два бинарника с одинаковым imphash с высокой вероятностью собраны из одного исходника или одним билдером. Это позволяет группировать семплы одного семейства, находить связи между кампаниями и фильтровать дубликаты в потоке.В
pefile вычисление - вызов pe.get_imphash(). Результат сравнивается с базой известных хешей (собственной или публичной, из репозиториев MISP). Совпадение с known-bad - немедленный escalation. Совпадение с known-good (imphash легитимного notepad.exe, calc.exe) - понижение score.Подозрительные паттерны API
Набор импортов, указывающий на конкретное вредоносное поведение:| Паттерн импортов | Вероятное поведение |
|---|---|
[URL='https://attack.mitre.org/techniques/T1055/']VirtualAllocEx[/URL] + WriteProcessMemory + CreateRemoteThread | Process injection (T1055, Defense Evasion / Privilege Escalation) |
CreateToolhelp32Snapshot + Process32First/Next | Перечисление процессов перед injection |
RegSetValueExA/W с ключами Run | Persistence через автозагрузку |
InternetOpenA + HttpSendRequestA | HTTP-based C2 коммуникация |
CryptEncrypt + FindFirstFile + MoveFileEx | Шифрование файлов (ransomware-паттерн) |
NtUnmapViewOfSection + VirtualAllocEx | Process hollowing (T1055.012) |
Одно наличие подозрительного импорта - не вердикт.
VirtualAllocEx используют легитимные программы: дебаггеры, антивирусы. Triage-скрипт должен учитывать комбинации и контекст: VirtualAllocEx + WriteProcessMemory + CreateRemoteThread в бинарнике с высокой энтропией, малым числом секций и аномальным EP - это escalation, а не drop.Rich Header
Rich header - недокументированная структура, которую добавляет линкер Microsoft. Содержит информацию о версиях компилятора и линкера, использованных при сборке. Малварь, собранная на MinGW, Go или Rust, Rich header не содержит. Легитимные бинарники Microsoft - содержат практически всегда.Аномалии Rich header: обнулённый или отсутствующий Rich header в файле, который позиционирует себя как продукт Microsoft. Или наоборот - Rich header с версиями MSVC 2008 в файле с timestamp 2025 года. Через
pefile доступно обращение к pe.RICH_HEADER, где каждая запись содержит compid - пару (product ID, version, object count).Installer framework detection: фильтрация шума в потоке
Значительная доля PE-файлов в потоке - легитимные инсталляторы: NSIS, InnoSetup, InstallShield, WiX. Их детектирование на этапе триажа экономит десятки минут аналитического времени.Маркеры основных фреймворков:
- NSIS - строки
NullsoftиNSISв overlay, секция.ndata, ресурс с типомNSIS - InnoSetup - строка
Inno Setupв overlay, характерная структураsetup.binвнутри - InstallShield - строки
InstallShield, ресурсы с типомRCDataопределённой структуры - WiX - встроенный MSI-файл, строки
Windows Installer XML - PyInstaller - строка
MEIв overlay, ресурс с иконкой Python
max(section.PointerToRawData + section.SizeOfRawData) для каждой секции. Всё, что после, - overlay. У нормального бинарника overlay отсутствует или минимален. У инсталлятора overlay составляет 90%+ размера файла - там хранится сжатый payload.Тут есть подвох: малварь тоже использует NSIS и InnoSetup как обёртки для доставки. Детект инсталлятора - не причина для автоматического drop. Это причина для отдельной ветки триажа: извлечь вложенные файлы из инсталлятора и прогнать их через pipeline рекурсивно. Потренировавшись «на кошках», быстро понимаешь, что инсталлятор-обёртка - один из самых частых трюков доставки.
Автоматизация malware triage: сборка пайплайна
Decision tree: логика принятия решений
Финальный pipeline объединяет все проверки в последовательный decision tree:- Парсинг PE-заголовков - если файл не парсится как PE, передать другому анализатору (ELF, Mach-O, документ)
- Installer detection - если инсталлятор, извлечь вложенные файлы, прогнать рекурсивно
- Imphash lookup - сравнение с базой known-bad и known-good
- Энтропия секций - если максимальная энтропия > 7.2 и число импортов < 10, пометить как packed
- YARA scan - если есть срабатывания, классифицировать по метаданным правила
- Анализ импортов - если подозрительные комбинации API, повысить severity
- Финальный вердикт: clean / suspicious / malicious / needs_manual_review
Python:
import pefile
def triage(path):
try:
pe = pefile.PE(path)
except pefile.PEFormatError:
return {"verdict": "not_pe"}
score = sum(20 for s in pe.sections if s.get_entropy() > 7.2)
ep = pe.OPTIONAL_HEADER.AddressOfEntryPoint
ep_section = next((s for s in pe.sections if s.VirtualAddress <= ep < s.VirtualAddress + s.Misc_VirtualSize), None)
if ep_section and ep_section != pe.sections[0]: # EP не в первой секции
score += 30
if pe.FILE_HEADER.TimeDateStamp == 0:
score += 10
return {"verdict": "suspicious" if score > 40 else "review", "score": score}
yara.compile(), imphash-lookup по словарю, installer detection через overlay analysis и логирование результатов в JSON для последующей агрегации.Ограничения статического триажа
Статический бинарный анализ без выполнения принципиально ограничен:Многослойная упаковка - payload зашифрован внутри пакера, который упакован другим пакером. Статика видит только внешний слой. Та самая матрёшка, которую не развернёшь без запуска.
.NET и managed code - для .NET-сборок триаж по нативным импортам бесполезен: единственный импорт -
mscoree.dll!_CorExeMain. Нужен отдельный pipeline с парсингом метаданных CLR через dnSpy, de4dot или YARA-модуль dotnet.Fileless-стадии - PE может быть дроппером, а основная логика загружается в память из C2. Триаж дроппера покажет минимальные импорты и маленький размер, но payload останется за кадром.
Go и Rust бинарники - статически слинкованные, с огромной таблицей символов и нестандартной структурой секций. Стандартные YARA-правила для C/C++ PE дают false negative. Нужны отдельные правила: для Go - строки
go.buildid и runtime-паттерны, для Rust - паникующие строки и манглинг символов.Антианализ - малварь может содержать мусорные импорты для обмана триажа (import table stuffing) или поддельный Rich header.
Статический триаж - фильтр первого эшелона. Он сокращает очередь на порядок, но не заменяет динамический анализ для семплов, прошедших порог подозрительности.
Я строю подобные pipeline третий год, и главное наблюдение - не в инструментах, а в пороговых значениях. Каждая команда калибрует thresholds под свой поток: у SOC банка и у вендора антивируса распределение чистых и вредоносных PE кардинально различается. Начинал с энтропии > 7.0 как порога для packed - и утонул в false positive от легитимных UPX-бинарников и .NET-сборок с ConfuserEx. Поднял до 7.2 и добавил условие на число импортов < 10 - точность выросла, но пропустил один семпл с кастомным XOR-пакером, у которого энтропия была 6.9. Абсолютных порогов не существует - есть порог, оптимальный для конкретного потока, и его нужно пересматривать каждый квартал.
Индустрия переоценивает сложность автоматизации malware triage. Пайплайн из 200 строк на Python с pefile + YARA + imphash-база покрывает большинство задач. Оставшиеся 15% - .NET, Go, Rust и кастомно упакованные семплы, для которых нужны специализированные ветки. Вместо построения универсального анализатора эффективнее держать набор специализированных pipeline с общим оркестратором. Обратная сторона: такой подход требует дисциплины. Каждый новый билдер малвари, каждый новый пакер - это новое YARA-правило и, возможно, новая ветка в decision tree. Кто перестаёт обновлять правила - через полгода получает pipeline, пропускающий половину входящего потока. Если хочется потренироваться на разборе packed PE руками - на HackerLab.pro в RE-категории есть задачи, где payload нужно извлечь через несколько слоёв обфускации без подсказок.