Реверс стилера в Ghidra: снимаем антиотладку и восстанавливаем шифрование конфига

Получил из песочницы ANY.RUN сэмпл инфостилера — PE-файл на 380 КБ, детект больше 40 движков на VirusTotal. Подцепил его в x64dbg — процесс умер за доли секунды, не дойдя даже до первого сетевого вызова. Знакомо? Антиотладочные проверки убивают процесс раньше, чем ты успеваешь поставить второй брейкпоинт. Остался реверс стилера в Ghidra: статический разбор без запуска бинарника. За вечер из сэмпла вытащил три C2-адреса, полный список целевых браузерных профилей и алгоритм шифрования конфига. Ниже — пошаговый разбор, от первой проверки IsDebuggerPresent до финального XOR-ключа.
Зачем стилеру антиотладка и зашифрованный конфиг
Стилер — вредоносная программа, которая крадёт учётные данные: пароли из браузеров, cookies сессий, файлы криптокошельков, токены мессенджеров. Семейства вроде RedLine, Vidar, Raccoon работают по одной схеме: оказались на машине жертвы, собрали данные, отправили на C2-сервер (Command & Control — удалённый сервер, куда стилер отгружает украденное).
Чтобы понять, зачем стилеру антиотладка и шифрование, полезно увидеть его место в цепочке атаки (kill chain — последовательность шагов от первого контакта с жертвой до конечной цели злоумышленника):
- Initial Access — жертва скачивает «кряк», «активатор» или открывает вложение из фишингового письма
- Execution — лоадер запускает стилер: иногда прямым запуском .exe, иногда инъекцией в легитимный процесс
- Defense Evasion — стилер проверяет, не работает ли он под отладчиком или в песочнице. В классификации MITRE ATT&CK (открытая база тактик и техник атак; T-коды вроде T1622 — её идентификаторы, по которым аналитики маркируют поведение малвари) это Debugger Evasion (T1622). Конфиг зашифрован, чтобы антивирус не вытащил C2-адрес сигнатурным поиском — Obfuscated Files or Information (T1027)
- Credential Access — стилер расшифровывает конфиг, узнаёт, какие данные красть, и обходит целевые пути: профили Chrome, Firefox, Telegram, криптокошельки (Credentials In Files, T1552.001)
- Exfiltration — собранный архив уходит HTTP-запросом на C2
Зачем автору малвари шифровать конфиг? Две причины. Первая — C2-адрес в открытом виде мгновенно детектируется антивирусом по сигнатуре, и сэмпл блокируется ещё до запуска. Вторая — конкурирующие группировки парсят чужие стилеры, вытаскивают адреса C2-панелей и перехватывают логи. Шифрование конфига усложняет оба сценария.
Антиотладка решает смежную задачу: стилер должен отработать быстро — собрать данные за секунды и самоуничтожиться. Если аналитик подцепит отладчик, он увидит расшифрованный конфиг в памяти, извлечёт C2 и отправит abuse-запрос хостеру. Антиотладочные проверки дают стилеру фору: бинарник молча завершается, если чувствует дебаггер, и аналитику приходится переходить к статическому анализу вредоносного ПО, чтобы добраться до конфига.
Что нужно для работы
Анализировать вредоносный сэмпл на хостовой ОС нельзя: случайный двойной клик — и стилер отработает по назначению.
Требования к окружению:
- Виртуальная машина: Windows 10 или 11 в VirtualBox / VMware. Сетевой адаптер внутри ВМ отключён. RAM для ВМ: минимум 4 ГБ, рекомендуется 8 ГБ — Ghidra на JVM активно ест память при авто-анализе
- Ghidra 11.x — скачивается с GitHub (NationalSecurityAgency/ghidra). Установки нет: распаковка архива, запуск
ghidraRun.bat. Требует JDK 17+, скачать с adoptium.net - Python 3.10+ — для скриптов дешифровки, установлен внутри ВМ
- Сэмпл — из MalwareBazaar, ANY.RUN или Triage. Работаем с выгруженным PE-файлом. Не запускаем его вручную
- Опционально: x64dbg для динамической верификации гипотез после снятия антиотладки; CFF Explorer для быстрого просмотра PE-заголовков
Первый взгляд: импорт сэмпла и разведка в Ghidra
Импортируйте сэмпл в Ghidra: File → New Project → Non-Shared Project, перетащите файл в окно проекта, откройте двойным кликом. На предложение запустить авто-анализ — соглашайтесь с настройками по умолчанию. Ghidra разберёт импорты, восстановит функции и найдёт строки. Ожидаемое время: от 10 секунд до пары минут в зависимости от размера бинарника. Когда прогресс-бар внизу справа пропадёт — анализ завершён.
Первое, что стоит проверить — упакован ли сэмпл (Software Packing, T1027.002). Признаки упаковки: откройте Window → Defined Strings. Если строк меньше 20–30 и среди них нет ничего осмысленного (ни имён WinAPI, ни путей к файлам) — бинарник почти наверняка упакован. Второй признак: в Program Trees одна-две секции с энтропией выше 7.0 (проверяется через Window → Entropy). UPX-пакер оставляет характерную сигнатуру UPX! в первых байтах — видно в листинге по адресу 0x0. Если сэмпл упакован — перед анализом нужна распаковка стилера: для UPX достаточно upx -d sample.exe, для кастомных пакеров придётся дампить память после распаковки в рантайме через x64dbg.
Если строк много (несколько сотен), видны пути к файлам, HTTP-заголовки, имена функций — бинарник не упакован, двигаемся дальше.
Откройте Symbol Tree → Imports (левая панель). Здесь перечислены DLL и вызываемые из них функции. Ищите характерные имена: IsDebuggerPresent, CheckRemoteDebuggerPresent, NtQueryInformationProcess, GetTickCount64, QueryPerformanceCounter. Каждое из этих имён — маркер антиотладочного приёма. Двойной клик на импорте → правый клик → References → Show References To Address покажет все места в коде, где вызывается эта функция.
Антиотладочные приёмы малвари в декомпиляторе Ghidra
IsDebuggerPresent и PEB-проверки
IsDebuggerPresent — самая элементарная проверка. Функция из kernel32.dll возвращает 1, если к процессу подключён отладчик, и 0, если нет. В декомпиляторе Ghidra (правая панель Code Browser) это выглядит так:
if (IsDebuggerPresent() != 0) {
ExitProcess(0);
}
Некоторые сэмплы не вызывают WinAPI напрямую, а читают флаг BeingDebugged из PEB (Process Environment Block — структура в памяти процесса, где Windows хранит отладочные флаги и метаданные). Реализуется через обращение к fs:[0x30] (в 32-битном PE) или gs:[0x60] (в 64-битном) с последующим чтением байта по смещению +0x2. Результат тот же: ненулевой байт — процесс завершается.
Более хитрый вариант — NtQueryInformationProcess с аргументом ProcessDebugPort (0x07). Эта функция из ntdll.dll возвращает ненулевое значение, если процесс отлаживается. В Ghidra импорт может быть не разрешён по имени — ищите вызов через GetProcAddress(hNtdll, "NtQueryInformationProcess") и последующий indirect call через указатель.
[Применимо: PE-бинарники x86/x64, стандартные стилеры без виртуализации кода. Не работает, если проверка скрыта внутри виртуализированного обфускатора (VMProtect, Themida) — Ghidra покажет вызовы в виртуальную машину обфускатора, а не реальную логику]
Timing-checks: GetTickCount64 и rdtsc
Замер времени выполнения — вторая по частоте техника. Логика простая: нормальное выполнение блока кода занимает микросекунды, но если аналитик шагает по инструкциям в дебаггере — секунды. Стилер замеряет время до и после контрольного участка, и если разница превышает порог (обычно 1000–5000 мс) — завершает процесс.
В Ghidra ищите пару вызовов GetTickCount64() или QueryPerformanceCounter() с последующим сравнением разницы. Паттерн в декомпиляторе: одна переменная получает значение до блока кода, вторая — после, затем if (end - start > threshold) ExitProcess(0). Переименуйте переменные (правый клик → Rename Variable) в start_time и end_time — паттерн сразу станет очевидным. Я всегда так делаю при первом проходе: переименование переменных экономит часы на втором.
Отдельная разновидность — инструкция rdtsc (Read Time-Stamp Counter), которая читает аппаратный счётчик тактов процессора. Она не отображается как импорт WinAPI, поэтому в Symbol Tree её нет. Ищите мнемонику RDTSC в ассемблерном листинге через Search → Program Text.
TLS Callbacks — код до main
TLS Callback (Thread Local Storage Callback) — функция, которую Windows вызывает до точки входа программы. Антиотладочные проверки внутри TLS Callback срабатывают раньше, чем отладчик успеет остановиться на main. Если вы поставили брейкпоинт только на entry point и процесс всё равно умирает — проверьте TLS.
Ghidra обнаруживает TLS Callbacks автоматически при импорте PE-файла. Ищите их в Symbol Tree → Functions — Ghidra может пометить их как tls_callback_0, tls_callback_1. Если такие функции содержат вызовы IsDebuggerPresent или сравнения с ProcessDebugPort — антиотладка спрятана именно здесь.
Обход антиотладки малвари: патчим проверки в Ghidra
Задача — нейтрализовать проверки, чтобы бинарник можно было запустить под отладчиком для верификации гипотез. Ghidra позволяет сделать это статическим патчингом: меняем инструкции прямо в листинге и экспортируем модифицированный бинарник.
Шаг 1. Найдите условный переход после проверки. В листинге (центральная панель) кликните на строку с вызовом IsDebuggerPresent. Декомпилятор подсветит соответствующий C-подобный код справа. Ниже в листинге будет инструкция TEST EAX, EAX (проверка возвращаемого значения), а за ней — условный переход JNZ или JNE (Jump if Not Zero / Jump if Not Equal), который направляет поток на блок с ExitProcess.
Шаг 2. Инвертируйте или занопте переход. Правый клик на инструкции JNZ → Patch Instruction. Два варианта:
— Замените JNZ на JZ (Jump if Zero) — теперь условие инвертировано, процесс продолжит работу при наличии отладчика
— Замените JNZ на два NOP (No Operation): байты 0x75 0xXX → 0x90 0x90. Проверка перестанет влиять на поток выполнения
Я обычно предпочитаю NOP — проще и нет риска перепутать логику, особенно когда проверок в бинарнике штук пять.
Шаг 3. Обработайте timing-check. Для проверок по времени — найдите инструкцию CMP (сравнение разницы с порогом) и следующий за ней JA (Jump if Above). Замените JA на NOP NOP либо измените пороговое значение через Patch Data на 0xFFFFFFFF — любая реальная задержка будет меньше.
Шаг 4. Проверьте TLS Callbacks. Если антиотладка размещена в TLS Callback — тот же подход: найдите условный переход внутри callback-функции и занопте его.
Шаг 5. Экспортируйте патченый бинарник. File → Export Program → Format: Binary. Сохраните как новый .exe.
Как понять, что получилось: запустите патченый бинарник в x64dbg внутри ВМ. Если раньше процесс умирал за доли секунды, а теперь доходит до сетевых вызовов (видно по вкладке Handles или логу системных вызовов) — антиотладка снята. Стилер может начать резолвить DNS и пытаться установить HTTP-соединение с C2 — это нормально, сеть в ВМ отключена, запрос никуда не уйдёт.
Восстановление алгоритма шифрования конфига стилера
После обхода антиотладки переходим к основной задаче — деобфускация конфига стилера. Конфиг содержит C2-адреса, список целевых приложений, иногда build ID и версию панели. Всё это хранится в зашифрованном виде внутри секции данных бинарника (Deobfuscate/Decode Files or Information, T1140).
Находим зашифрованный блоб
Первый способ: Window → Defined Strings в Ghidra. В типичном неупакованном стилере видны десятки читаемых строк — пути к профилям браузеров, HTTP-заголовки, имена WinAPI-функций. Среди них попадаются нечитаемые последовательности байтов фиксированной длины — кандидаты на зашифрованный конфиг.
Второй способ надёжнее. Ищите функцию, которая принимает указатель на массив байтов и длину, а возвращает указатель на расшифрованный буфер. Переходите по xref (перекрёстная ссылка — показывает, откуда вызывается функция или откуда читаются данные) от строк, которые стилер использует после расшифровки: "Content-Type", "Mozilla/5.0", "POST", "gate.php". Если строка нашлась в открытом виде — конфиг не зашифрован (редкость). Если нет — проследите вызовы функций, которые генерируют эти строки: одна из них будет дешифратором.
Третий маркер — функция, которая вызывается в самом начале main или WinMain, до любой полезной активности, и заполняет глобальный массив или структуру. Часто это инициализация конфига: расшифровка блоба в глобальную переменную, к которой потом обращаются десятки других функций. Если видите функцию с одним xref из main и кучей xref на глобальный массив — начинайте с неё.
Определяем схему: XOR, multi-byte XOR или RC4
Три самых распространённых алгоритма шифрования конфигов в commodity-стилерах (массовых стилерах, которые продаются как сервис):
Однобайтовый XOR. Каждый байт конфига XOR’ится с одним и тем же ключом (например, 0x55). В декомпиляторе Ghidra это цикл с операцией buf[i] = buf[i] ^ key, где key — одна переменная без индексации. Самый простой вариант: если ключ — единственный аргумент функции помимо указателя и длины.
Multi-byte XOR. Массив XOR’ится с ключом длиной N байт по формуле buf[i] = buf[i] ^ key[i % key_len]. В декомпиляторе появляется операция % (остаток от деления) или два вложенных счётчика. Ключ обычно лежит рядом с зашифрованными данными в секции .data или .rdata. Длина ключа — от 4 до 32 байт.
RC4. Потоковый шифр с двумя фазами: KSA (Key Scheduling Algorithm — инициализация массива S-box из 256 элементов) и PRGA (Pseudo-Random Generation Algorithm — генерация потока для XOR). В Ghidra RC4 распознаётся по характерному паттерну: два цикла до 256 в первой части функции и swap-операции с XOR во второй. Если в декомпиляторе видите локальный массив на 256 элементов типа byte, два цикла с i от 0 до 255, и операции вида temp = s[i]; s[i] = s[j]; s[j] = temp — перед вами RC4. Этот паттерн настолько узнаваемый, что со временем начинаешь видеть его за секунду — как знакомое лицо в толпе.
Ключ к RC4 часто хардкодится строкой или массивом байтов. Ищите первый аргумент, передаваемый в функцию с 256-элементным массивом — это и есть ключ.
Пишем дешифратор и вытаскиваем C2
Когда алгоритм определён и ключ найден — дело за скриптом. Для multi-byte XOR:
encrypted = bytes.fromhex("4a1f3c...") # скопировать из Ghidra: Copy Special → Byte String
key = b"\x55\xAA\x0F\x31" # ключ из секции .rdata
decrypted = bytes([b ^ key[i % len(key)] for i, b in enumerate(encrypted)])
print(decrypted.decode("utf-8", errors="replace"))
Чтобы скопировать зашифрованный блоб: выделите диапазон байтов в листинге Ghidra → правый клик → Copy Special → Byte String (No Spaces). Ключ берётся аналогично — по адресу, который передаётся вторым аргументом в функцию-дешифратор.
Ожидаемый результат: в консоли появятся читаемые строки — URL вида http://185.X.X.X/gate.php, пути типа \Google\Chrome\User Data\Default\Login Data, иногда build ID. Если вместо читаемого текста — мусор, проверьте три вещи: правильно ли скопирован блоб (длина совпадает?), верный ли ключ (адрес?), верный ли алгоритм (может быть RC4, а не XOR).
Для RC4 — реализуйте KSA и PRGA на Python вручную (20 строк) или возьмите библиотеку arc4 (pip install arc4). Принцип тот же: передаёте ключ и зашифрованный буфер, получаете расшифрованный конфиг.
Каждый извлечённый C2-адрес — actionable IOC (Indicator of Compromise, индикатор компрометации — конкретный технический артефакт, по которому можно обнаружить заражение или заблокировать угрозу). Его можно передать в CERT, добавить в блоклист фаервола, использовать для threat intelligence. Список целевых приложений из конфига показывает, что именно стилер собирает — это помогает оценить масштаб ущерба при реальном инциденте.
Ограничения: когда анализ вредоносного сэмпла этим путём не пройдёт
Описанный подход работает для массовых стилеров с типовой антиотладкой и стандартным шифрованием. Вот ситуации, когда его недостаточно:
| Сценарий | Почему не работает | Альтернатива |
|---|---|---|
| Виртуализация кода (VMProtect, Themida) | Ghidra декомпилирует вызовы к обфускатору, не к реальной логике | Динамический анализ с дампом после девиртуализации |
| Ключ шифрования приходит с C2 | В бинарнике нет ключа — конфиг расшифровывается только после запроса к серверу | Перехват трафика через FakeNet-NG, эмуляция C2-ответа |
| Кастомный криптоалгоритм | Не XOR, не RC4 — авторская схема без узнаваемых паттернов | Полный ручной reverse engineering шифрования конфига |
| Многослойная упаковка | Несколько уровней: UPX → custom loader → in-memory payload | Послойная распаковка в x64dbg с промежуточными дампами |
| Сэмпл на .NET / Python | Ghidra слабее специализированных инструментов для managed-кода | dnSpy для .NET, uncompyle6 для Python |
Декомпилятор Ghidra тоже не идеален. Он может неправильно восстановить типы переменных, пропустить indirect call через указатель, выдать нечитаемый код для оптимизированных функций. Если декомпилированный вывод не складывается — переключайтесь на ассемблер в листинге. Ghidra подсвечивает соответствие: клик по строке в декомпиляторе выделяет ассемблерные инструкции в листинге и наоборот. Это сопоставление — лучший способ разобраться, когда декомпилятор врёт.
Для практики рекомендую брать сэмплы с MalwareBazaar (bazaar.abuse.ch) — там есть фильтр по тегам семейств (redline, vidar, raccoon), и каждый сэмпл доступен с хэшем и метаданными. Начинайте с тех, что помечены тегом exe без тега packed — это упростит первый разбор. На HackerLab.pro есть задачи на реверс, которые помогают отточить навык работы с дизассемблером до перехода к реальной малвари — нужна регистрация, после неё доступны таски разных уровней.
Антиотладка в commodity-стилерах — это копипаста. Из нескольких десятков разобранных сэмплов RedLine и Vidar больше половины использовали один и тот же набор: IsDebuggerPresent, иногда GetTickCount64, редко TLS Callback. Авторы стилеров не тратят время на защиту от реверса — они вкладываются в скорость распространения и смену C2-инфраструктуры. Новый билд с новым C2-адресом появляется каждые несколько часов, и к моменту, когда аналитик закончит разбор, адрес уже может быть мёртв.
Отсюда неочевидный вывод: главный навык малвар-аналитика — не реверс сам по себе, а автоматизация реверса. Один раз разобрали алгоритм шифрования конкретного семейства — написали скрипт на Python или Ghidra API, который вытаскивает конфиг из любого нового билда за секунды, без ручного ковыряния каждого сэмпла. Здесь проходит граница между junior-аналитиком, который каждый раз открывает Ghidra заново, и senior, у которого конвейер скриптов покрывает десятки семейств. Понимание XOR и RC4 на уровне байтов — точка входа. А точка роста — превратить одноразовый разбор в воспроизводимый pipeline.
Эту тему и смежные навыки разбирают на практике в курсе «Профессия Реверс-инженер» Codeby Academy.