На разборе инцидента в финансовой организации команда получила PE-файл размером 847 КБ: ни одного экспортируемого символа, энтропия секции
.text - 7.92 из 8, IAT содержит ровно две записи - LoadLibraryA и GetProcAddress. Ghidra на автоанализе выдал один гигантский блок с арифметическими операциями вместо осмысленного кода, а IDA Pro показала jmp на вычисляемый адрес в первом же базовом блоке. Дизассемблер и декомпилятор не врали - они честно показали то, что лежало в файле. Реальный код ещё не существовал: полезная нагрузка распаковывалась в рантайме через цепочку из четырёх техник по классификации MITRE ATT&CK. Именно такие бинарники составляют основу работы malware-аналитика, и на них подход «загрузить в дизассемблер и почитать» ломается за первую минуту.Эта статья - системный workflow reverse engineering бинарного кода, построенный вокруг реальных антианализных техник: от триажа до восстановления C2-логики из распакованного payload. Не «ещё один гайд по Ghidra», а методология, привязанная к конкретным MITRE-идентификаторам.
Триаж бинарных файлов: первые пять минут формируют стратегию
Анализ бинарных файлов начинается не с дизассемблера, а с триажа - быстрой классификации, которая определяет дальнейший workflow. За первые пять минут нужно ответить на три вопроса: что за формат, упакован ли семпл, какие антианализные техники предположительно применены. Если ошибиться здесь - потеряешь час, ковыряя stub-код упаковщика вместо реального payload.Требования к окружению
- ОС: Windows 10/11 в изолированной ВМ (VirtualBox 7.x / VMware Workstation), сеть отключена или host-only adapter
- RAM: минимум 8 ГБ для ВМ (Ghidra + x64dbg одновременно съедают 4-6 ГБ)
- Диск: 60 ГБ минимум (Ghidra-проекты разрастаются быстро)
- Инструменты триажа: Detect-It-Easy (DiE) 3.x, PE-bear 0.6+, radare2 6.1.5 (rabin2),
file/readelfдля ELF - Обязательно: снапшот «чистого» состояния ВМ до начала работы с семплом - откат за 30 секунд вместо переустановки. Не пренебрегайте этим, один раз я потерял полдня из-за криво отработавшего семпла, который покорёжил системные библиотеки
Decision tree: выбор подхода к анализу исполняемых файлов
Выбор между статическим и динамическим анализом - не вопрос предпочтений, а следствие результатов триажа:| Условие | Подход | Инструмент первого выбора |
|---|---|---|
| Энтропия .text < 6.5, IAT > 20 записей, пакер не обнаружен | Статический анализ | Ghidra / IDA Pro |
| Энтропия > 7.0, IAT пустой или минимальный (1-5 записей), DiE определяет UPX/Themida/VMProtect | Динамический - распаковка - статический | x64dbg, затем Ghidra для дампа |
| PE с CLR-заголовком (.NET), обфускатор не обнаружен | Статический анализ IL | dnSpy / ILSpy |
| .NET + ConfuserEx / Reactor / другой обфускатор | Деобфускация, затем статический | de4dot, затем dnSpy |
| ELF stripped, нет символов, не упакован | Статический с ручной аннотацией | Ghidra + radare2 для скриптового анализа |
| Любой формат с антиотладочными проверками | Динамический с обходом антиотладки | x64dbg + ScyllaHide |
Первый шаг триажа - определение формата и упаковки. Команда
rabin2 -I <file> из набора radare2 выдаёт тип файла, архитектуру, эндианность и энтропию за секунду. DiE (Detect-It-Easy) дополняет сигнатурным определением компилятора и пакера - для PE-файлов DiE распознаёт сотни сигнатур упаковщиков и протекторов.Ключевой индикатор - импортная таблица. Если IAT содержит только
LoadLibraryA, GetProcAddress и, может быть, VirtualAlloc - перед вами с высокой вероятностью Dynamic API Resolution (T1027.007, Defense Evasion). Реальные API-вызовы скрыты и будут резолвиться в рантайме через хеширование имён функций. Статический анализ в чистом виде здесь бесполезен: декомпилятор покажет вызовы через вычисляемые указатели без имён, и вы будете смотреть на call [eax] до посинения.Второй индикатор - секции. Стандартный PE, скомпилированный MSVC, содержит
.text, .rdata, .data, .rsrc. Секции с именами UPX0, .vmp0, .themida или произвольными строками - признак Software Packing (T1027.002, Defense Evasion). Упаковщик сжимает или шифрует оригинальный код, а при запуске stub-код распаковывает его в память.[Применимо: триаж универсален для IR, threat hunting, malware research и CTF-категории RE. Контекст значения не имеет - это отправная точка любого анализа.]
Инструменты реверс инжиниринга: когда декомпилятор врёт
Русскоязычные гайды сводят выбор инструментов к «Ghidra бесплатный, IDA платный». Это упрощение скрывает архитектурные различия, которые определяют качество анализа в конкретных сценариях. На практике аналитик переключается между несколькими инструментами - и это нормально, не нужно выбирать «один на всю жизнь».Сравнение по архитектурным критериям
| Критерий | IDA Pro 9.x (2024) | Ghidra 11.x (2024) | radare2 6.1.5 (активная разработка) | Binary Ninja 4.x (2024) |
|---|---|---|---|---|
| Декомпилятор x86/x64 | Hex-Rays - лучшая точность на оптимизированном коде | Встроенный, бесплатный, корректен в ~85% случаев | Через r2ghidra-плагин (движок Ghidra) | Встроенный HLIL/MLIL, альтернативный подход через уровни IL |
| Обфусцированный код | gooMBA - деобфускация MBA-выражений из коробки | Требует кастомных скриптов | r2pipe для автоматизации | Частично через SSA-форму |
| Скриптовый API | IDAPython (зрелая среда, сотни плагинов) | Ghidra Script (Java + Jython) | r2pipe (Python/JS/Go) + встроенная командная оболочка | Python API с типизацией |
| Встроенная отладка | Да (win/linux/mac/embedded) | Нет | Да (CLI через r2 debug) | Нет |
| FLIRT/FID (идентификация библиотек) | FLIRT - зрелая технология с большой базой сигнатур | Function ID - аналог, но база меньше | Через сигнатуры zignatures | Через signature libraries |
| Цена (Named license) | от $1975/год | Бесплатно (Apache 2.0) | Бесплатно (LGPL3) | от $299 (personal) |
[Применимо: для single-sample IR или CTF - Ghidra хватит за глаза. Для ежедневной работы с обфусцированными семплами в коммерческом SOC/DFIR - IDA Pro окупается за первый месяц. radare2 - для CLI-автоматизации и скриптовых пайплайнов, когда нужно прогнать 50 семплов за ночь.]
Конкретные сценарии, где декомпилятор генерирует некорректный псевдокод
Непрямые вызовы через вычисляемые адреса. При Dynamic API Resolution (T1027.007) декомпилятор видитcall [eax], но понятия не имеет, какая функция вызывается. Результат: ([I](void ([/I]*)(void))local_18)() вместо осмысленного CreateProcessW(...). Решение: в x64dbg дойти до вызова, зафиксировать реальный адрес, затем в Ghidra/IDA вручную задать тип функции через «Edit Function Signature». Муторно, но другого пути нет.Самомодифицирующийся код. Если код перезаписывает себя в рантайме - типовой приём в распаковщиках - статический декомпилятор анализирует stub, а не payload. Единственный выход: дать коду отработать распаковку в отладчике, сдампить память, загрузить дамп заново. Пересказывать спеку дизассемблера в сотый раз не вижу смысла - он тут бессилен.
Оптимизация tail calls. Компиляторы с
-O2 и выше заменяют call + ret на jmp, что ломает boundary detection. Ghidra может объединить две функции в одну или «потерять» хвостовой вызов. IDA Pro обрабатывает это точнее за счёт эвристик Hex-Rays, но тоже не безупречна. Workaround: вручную определить границу функции (P в IDA, «Create Function» в Ghidra).Control flow flattening. Превращает граф потока управления в один switch-case с диспетчером. Декомпилятор честно показывает switch - но восстановить исходную логику без скриптовой деобфускации практически невозможно. IDA Pro поставляется с плагином gooMBA для MBA-выражений; у Ghidra из коробки аналога нет, придётся писать свой или искать на GitHub.
Статический анализ вредоносного ПО: упаковка, стриппинг и скрытие API
Статический анализ вредоносного ПО - работа с кодом без его запуска. Плюс: полный обзор бинарника, свободная навигация в любом направлении. Минус: упакованный или обфусцированный семпл показывает красивый фантик, а не реальный код. Ниже - ключевые антианализные техники, с которыми аналитик сталкивается ежедневно.Software Packing (T1027.002, Defense Evasion)
Упаковка - самая распространённая техника сокрытия payload. Семпл содержит stub-код (распаковщик) и зашифрованные или сжатые данные. При запуске stub выделяет память черезVirtualAlloc, расшифровывает payload, копирует его в выделенную область и передаёт управление. Классическая матрёшка.Идентификация при триаже: энтропия > 7.0, минимальный IAT, нестандартные имена секций. DiE определяет известные пакеры по сигнатуре - UPX, ASPack, Themida, VMProtect. Кастомные пакеры (а в APT-малвари они почти все кастомные) сигнатурно не детектируются - нужен ручной анализ stub-кода.
Для UPX достаточно
upx -d <file>, но авторы малвари часто модифицируют UPX-заголовок (меняют магические байты UPX!), чтобы штатная распаковка не сработала. В этом случае: восстановить заголовок вручную в HxD или распаковывать динамически через x64dbg. Второй вариант надёжнее - он работает для любого пакера.[Ограничения: VMProtect и Themida используют виртуализацию опкодов - реальные инструкции заменены байткодом кастомной VM. Статическая распаковка неэффективна. Подход: только динамический анализ с дампом после полной распаковки. EDR-решения (CrowdStrike Falcon, SentinelOne) флагают аллокацию RWX-памяти как подозрительную, но это не блокирует распаковку - только сигнализирует аналитику.]
Stripped Payloads (T1027.008, Defense Evasion)
Stripped-бинарники - файлы, из которых удалена отладочная информация и таблица символов. Для ELF -strip -s, для PE - компиляция без PDB. В Ghidra вместо осмысленных имён: FUN_00401230, FUN_004013a0. Тоска.Стратегия: восстановление сигнатур стандартных библиотек. IDA Pro использует FLIRT (Fast Library Identification and Recognition Technology) - сравнивает байтовые паттерны с базой сигнатур известных компиляторов и runtime-библиотек. Ghidra предлагает Function ID (FID) с аналогичным принципом, но база поменьше.
После FLIRT/FID-идентификации стандартных функций остаются только авторские. Их именование - ручная работа: анализ аргументов, возвращаемых значений, вызываемых API. Это самая трудоёмкая часть исследования вредоносных программ, и ускорить её можно только опытом - со временем начинаешь узнавать характерные паттерны: вот это похоже на инициализацию сокета, а вот это - на обход каталогов.
Dynamic API Resolution (T1027.007, Defense Evasion)
Вместо явного импортаCreateProcessW малварь в рантайме вычисляет хеш от имени функции, перебирает экспортную таблицу DLL и находит совпадение. В IAT - только LoadLibraryA/GetProcAddress или вообще ничего (при ручном парсинге PEB → LDR → InMemoryOrderModuleList).Распространённые хеш-алгоритмы: ROR13 (rotate right 13, классика Metasploit shellcode), DJB2, CRC32, MurmurHash. В дизассемблере паттерн выглядит как цикл, внутри которого
movzx читает байт за байтом, затем ror/xor/add, и на выходе cmp с 32-битной константой. Эта константа - хеш искомой API-функции. Узнаёте паттерн один раз - потом видите его везде.Практический подход к обратной разработке программ с Dynamic API Resolution: опознать алгоритм хеширования, написать скрипт на Python, который вычисляет хеши всех функций из
kernel32.dll, ntdll.dll, user32.dll, построить lookup-таблицу, затем через Ghidra Script или IDAPython автоматически заменить константы на комментарии с именами API. Один раз написал скрипт - используешь на десятках семплов.Deobfuscate/Decode Files or Information (T1140, Defense Evasion)
Данные - строки, конфигурация C2, дополнительные модули - хранятся в зашифрованном виде и расшифровываются в рантайме. Типичные алгоритмы: XOR с однобайтовым или многобайтовым ключом (самый частый случай), RC4, base64 + XOR в комбинации.В статическом анализе XOR-шифрование обнаруживается по паттерну: загрузка ключа в регистр, цикл по буферу с
xor byte [esi+ecx], al, инкремент счётчика. Для автоматической расшифровки - идентифицируем функцию дешифровки (её вызывают многократно, перед каждым использованием строки), извлекаем алгоритм и ключ, пишем скрипт для bulk-расшифровки. Скриптовая расшифровка строк - один из финальных навыков в reverse engineering, потому что она требует полного понимания calling convention и layout памяти. Но когда скрипт заработает и выплюнет все C2-адреса разом - это того стоит.Динамический анализ бинарников: обход антиотладки и трассировка кода
Динамический анализ - запуск семпла в контролируемой среде с отладчиком. Плюс: видите код после распаковки и расшифровки, ground truth вместо догадок. Риск: малварь активно противодействует отладке и трассировке кода, и порой делает это изобретательно.Debugger Evasion (T1622, Defense Evasion / Discovery)
Антиотладочные техники - проверки для обнаружения отладчика. При срабатывании: аварийное завершение, переход на фейковую ветку кода или тихое изменение поведения. Последний вариант - самый опасный: код работает, но делает безвредные вещи, и вы анализируете пустышку, даже не подозревая об этом.IsDebuggerPresent - читает флаг
BeingDebugged из PEB (Process Environment Block). В дизассемблере: call IsDebuggerPresent → test eax, eax → jnz <exit_branch>. Обход в x64dbg: breakpoint на IsDebuggerPresent, при срабатывании - изменить EAX на 0 в панели регистров. Или проще: плагин ScyllaHide (активно поддерживается), который патчит PEB автоматически.NtQueryInformationProcess с
ProcessDebugPort (class 0x7) - проверка через ntdll, более надёжная, чем IsDebuggerPresent. В дизассемблере: push 7 перед вызовом NtQueryInformationProcess. ScyllaHide перехватывает этот вызов и подменяет результат.Timing checks - замер времени через
rdtsc, QueryPerformanceCounter или GetTickCount. Отладчик замедляет выполнение, и если дельта между двумя замерами превышает порог - малварь считает, что её отлаживают. Обход сложнее: патчить проверку (заменить условный jnz на безусловный jmp или nop), либо использовать hardware breakpoints вместо программных (они не вносят int 3 в код и менее детектируемы). Но продвинутые пакеры проверяют DR-регистры через GetThreadContext - ScyllaHide с DRx protection блокирует и эту проверку. Гонка вооружений, как обычно.[Применимо: антиотладка встречается в реальной малвари всех классов - от массовых стилеров до APT-загрузчиков. В CTF-задачах RE антиотладка - стандартный элемент средней и высокой сложности.]
System Checks (T1497.001, Defense Evasion / Discovery)
Обнаружение виртуальных машин и песочниц. Цель: определить, что код работает не на реальной машине жертвы, а в аналитической среде.Типичные проверки:
cpuidсeax=1, бит 31ecx- Hypervisor present (бит выставляется гипервизором по соглашению; VMware/VirtualBox/Hyper-V его устанавливают, но продвинутые конфигурации, например KVM с cpu-pt, могут его очистить)- MAC-адреса: префиксы VMware (00:0C:29, 00:50:56), VirtualBox (08:00:27)
- Процессы: поиск
vmtoolsd.exe,VBoxService.exe,wireshark.exe,procmon.exe - Реестр:
HKLM\SOFTWARE\VMware, Inc.,HKLM\SOFTWARE\Oracle\VirtualBox - Файловая система:
C:\Windows\System32\drivers\vmmouse.sys
<hidden state='on'/> в конфигурации libvirt, что скрывает большинство гипервизорных артефактов. На практике я предпочитаю именно KVM - возни больше на этапе настройки, зато потом не нужно думать о каждой проверке отдельно.[Ограничения: продвинутые sandbox-evasion техники (проверка количества ядер CPU < 2, RAM < 4 ГБ, пустая история документов, отсутствие принтеров) обходятся только настройкой «живой» ВМ с пользовательской активностью. Придётся создать иллюзию обитаемой машины - документы, история браузера, пара принтеров.]
Дизассемблирование и декомпиляция инъекционных техник
Обратная разработка программ, использующих code injection, требует понимания цепочки API-вызовов и их аргументов в контексте полной kill chain - от allocation до execution. Тут нельзя смотреть на отдельный вызов - нужна вся последовательность.Process Hollowing (T1055.012, Defense Evasion / Privilege Escalation)
Создание легитимного процесса в suspended-состоянии, замена его образа в памяти на вредоносный, возобновление выполнения. Результат: малварь работает под именемsvchost.exe или explorer.exe. Красивый фантик - внутри совсем другое.Цепочка API-вызовов для поиска в дизассемблере:
Код:
CreateProcessW(..., CREATE_SUSPENDED, ...) // создание suspended-процесса
NtUnmapViewOfSection(hProcess, imageBase) // выгрузка легитимного образа
VirtualAllocEx(hProcess, imageBase, ...) // выделение памяти под наш PE
WriteProcessMemory(hProcess, imageBase, ...) // запись вредоносного PE
SetThreadContext(hThread, &ctx) // установка нового entry point
ResumeThread(hThread) // поехали
kernel32.dll. Совместный импорт CreateProcessW и NtUnmapViewOfSection - сильный индикатор. Если API резолвится динамически (T1027.007) - сначала восстановите имена через хеш-таблицу, иначе будете смотреть на call [eax] и гадать.В x64dbg: breakpoint на
WriteProcessMemory - в момент вызова в аргументах будет указатель на записываемые данные. Это и есть распакованный payload, который можно сдампить через плагин Scylla.CrowdStrike Falcon и Microsoft Defender for Endpoint (MDE) детектируют Process Hollowing через kernel callbacks и ETW-TI (Event Tracing for Windows - Threat Intelligence). Hook-first EDR перехватывают
NtUnmapViewOfSection на user-mode уровне. Знание этого помогает при threat hunting - аналитик SOC может построить правило на последовательность CreateProcess(SUSPENDED) → NtUnmapViewOfSection → WriteProcessMemory.Native API (T1106, Execution)
Вызов функций ntdll.dll напрямую вместо kernel32.dll - способ обойти мониторинг user-mode хуков. EDR-решения с hook-first архитектурой перехватываютCreateProcessW в kernel32, но не обязательно NtCreateUserProcess в ntdll. Малварь просто «проскальзывает» уровнем ниже.В дизассемблере: прямые вызовы
Nt[I]/Zw[/I] функций - NtAllocateVirtualMemory, NtWriteVirtualMemory, NtCreateThreadEx. Если малварь не импортирует их явно - резолвит через парсинг EAT (Export Address Table) ntdll.dll, что возвращает нас к T1027.007.В последних семплах встречается ещё более агрессивный подход: direct syscalls - копирование syscall-stub из ntdll и вызов
syscall напрямую из памяти малвари. Это обходит любые user-mode хуки, но детектируется через kernel-level телеметрию. Elastic EDR 8.x+ с Elastic Defend отслеживает аномальные syscall-источники (call stack не из ntdll), SentinelOne детектирует исполняемый код из unbacked memory regions. Гонка продолжается - но kernel-level телеметрия пока выигрывает.[Применимо: понимание Native API критично для threat hunting и IR. При пентесте - знание детектируемости помогает выбирать evasion-вектор под конкретный EDR-стек целевой инфраструктуры.]
Пошаговый workflow: от упакованного семпла до C2-конфигурации
Объединим описанные техники в практическую последовательность. Сценарий: получен PE-файл, подозрение на загрузчик, задача - извлечь адреса C2.
🔓 Часть контента скрыта: Эксклюзивный контент для зарегистрированных пользователей.
Шаг 1: Триаж (2 минуты).
rabin2 -I suspect.exe - архитектура, энтропия, импорты. DiE определяет пакер: сигнатура не найдена, но энтропия 7.8, IAT содержит 3 функции. Вывод: кастомный пакер. Статика бессмысленна, идём в отладчик.Шаг 2: Поверхностный статический анализ (5 минут). Загружаем в Ghidra. Entry point содержит
VirtualAlloc → цикл с XOR → call eax. Это stub распаковщика. Декомпилятор показывает: XOR-ключ по фиксированному смещению, зашифрованные данные в секции .rsrc. Записываем ключ и смещение - пригодится.Шаг 3: Динамическая распаковка (10-15 минут). Открываем в x64dbg. Breakpoint на
VirtualAlloc (bp VirtualAlloc). При срабатывании: смотрим размер аллокации и права (PAGE_EXECUTE_READWRITE - классика для распаковки, RWX-память - как красная тряпка). Даём отработать, ставим hardware breakpoint on execution на первый байт выделенного региона. При срабатывании hw bp - мы в распакованном коде. Если перед этим сработала антиотладка - включаем ScyllaHide с профилем «Aggressive».Шаг 4: Дамп и фиксация (5 минут). Плагин Scylla: IAT Autosearch → Get Imports → Dump. Scylla находит IAT в распакованном коде, фиксирует адреса, сохраняет восстановленный PE.
Шаг 5: Анализ распакованного payload. Дамп в Ghidra. Декомпилятор теперь выдаёт осмысленный код - совсем другое дело. Ищем C2-инициализацию: вызовы
InternetOpenA/HttpOpenRequestA (WinINet) или WSAStartup/connect (Winsock). Если строки зашифрованы (T1140) - идентифицируем функцию дешифровки, извлекаем алгоритм:
Python:
# Расшифровка XOR-строк из дампа
# Адаптировать под конкретный семпл
def xor_decrypt(data, key):
return bytes([b ^ key[i % len(key)] for i, b in enumerate(data)])
# Пример: xor_decrypt(b'\x3a\x1f\x0c', b'\x55')
# Результат: реальный C2-адрес или URI
Весь workflow занимает от 30 минут (UPX + XOR) до нескольких дней (VMProtect + multi-stage loader с sandbox evasion). Ключевой принцип: не пытаться сделать всё в одном инструменте. Дизассемблер и декомпилятор дают карту, отладчик - ground truth, скрипты автоматизируют рутину.
За последние два года я наблюдаю устойчивый сдвиг: авторы малвари всё реже используют «классическую» упаковку (UPX, ASPack) и всё чаще применяют кастомные загрузчики с многоступенчатой распаковкой - shellcode → reflective DLL → final payload. Каждая ступень задействует собственный набор антианализных проверок. Подход «найти OEP и сдампить» перестаёт работать, потому что OEP как единой точки не существует: код собирается из разных аллокаций по частям, как конструктор.
Из этого следует неудобный вывод: инструменты реверс инжиниринга менее важны, чем методология. Ghidra или IDA - второстепенный выбор по сравнению с умением правильно расставить breakpoints на memory allocation и отследить цепочку
VirtualAlloc → WriteProcessMemory → execution. Аналитик, который распознаёт паттерны MITRE ATT&CK на уровне дизассемблированного кода, работает быстрее того, кто знает все горячие клавиши IDA, но не видит T1027.007 в потоке инструкций.Этот разрыв стоит закрывать - и начинать имеет смысл не с очередного туториала по Ghidra, а с построения workflow вокруг конкретных антианализных техник. Попробуйте взять любой семпл с MalwareBazaar, прогнать через описанный pipeline и посмотреть, на каком шаге застрянете. Именно это место и нужно прокачивать. На WAPT эту цепочку проходят в течение двух модулей с лабами.