Перейти к содержимому

Анализ Lumma Stealer в Ghidra: какие техники антианализа видит статика и где нужен отладчик

Анализ Lumma Stealer в Ghidra: какие техники антианализа видит статика и где нужен отладчик
Время чтения: 13 мин.

Три семпла Lumma Stealer за одну неделю — и каждый упакован по-новому: другой пакер, другая схема шифрования строк, свежий набор C2-доменов. Но антиотладочные проверки внутри — те же PEB-флаги, те же timing-трюки, та же тригонометрия мыши. По данным IBM X-Force Threat Intelligence Index 2025, инфостилеры составили 32% всех образцов вредоносного ПО в 2024 году, и Lumma стабильно в числе самых активно обновляемых. Разбираю, что из техник антианализа ловится в Ghidra без единого запуска, а что проявляется только под отладчиком.

Анализ стилера Lumma: зачем реверсить и что внутри

Lumma Stealer (он же LummaC2) — инфостилер, написанный на C. Инфостилер — вредоносная программа, заточенная на кражу данных: паролей, cookie, криптокошельков. Распространяется по модели MaaS (Malware-as-a-Service): разработчик продаёт доступ к малвари за подписку, а «клиенты» используют её против своих жертв. Впервые обнаружен в августе 2022 года на русскоязычных теневых форумах. По данным «Лаборатории Касперского», к марту 2025 года стартовая цена подписки составляла 250 долларов, клиентская база — более тысячи активных подписчиков.

Стилер крадёт сохранённые пароли из браузеров, cookie-файлы сессий, данные автозаполнения, расширения криптокошельков (MetaMask, Phantom), файлы из пользовательских директорий. Украденное упаковывается в «лог» — ZIP-архив с предсказуемой структурой — и уходит на C2-серверы (серверы управления атакой) по HTTP POST.

Бизнес-логика атаки простая и циничная: украденные логи продаются на маркетплейсе LummaC2 от 1 до 100+ долларов за связку в зависимости от содержимого. Данные корпоративного VPN-доступа перепродаются initial access broker’ами (посредники, торгующие начальным доступом в сети) за сотни и тысячи долларов операторам ransomware. По данным Cloudflare, маркетплейс Lumma поддерживает поиск по доменам, странам и криптокошелькам — полноценная платформа монетизации.

Зачем реверсить, если есть готовые IOC (Indicators of Compromise — индикаторы компрометации: домены, хэши, IP-адреса)? Потому что каждый новый билд Lumma меняет пакер и ротирует C2-домены — IOC устаревают за дни. А поведенческие паттерны антианализа меняются значительно реже. Разобрав их один раз в Ghidra, можно писать детектирующие правила, которые живут месяцами.

Каждая техника антианализа в этом разборе маппится на MITRE ATT&CK — открытую базу тактик и техник атак, своего рода «периодическую таблицу» действий злоумышленников. T-коды (например, T1497, T1622) — её идентификаторы. Зачем они нужны: они позволяют единообразно описывать поведение малвари вне зависимости от конкретного семпла. Когда вы пишете в отчёте «T1622», любой аналитик в любом SOC понимает, о чём речь, без пересказа.

Ghidra статический анализ малвари: что видно без запуска

Статический анализ — изучение бинарного файла без его запуска. Загружаем семпл в Ghidra (бесплатный фреймворк обратной разработки от NSA), изучаем дизассемблированный код и вывод декомпилятора, ищем характерные паттерны. Ниже — то, что Ghidra показывает до первого запуска в отладчике.

Распаковка Lumma Stealer: определяем пакер

Почти все свежие билды Lumma приходят упакованными. Пакер — программа-обёртка, которая сжимает или шифрует реальный код малвари. Цель — затруднить анализ и обойти сигнатурный детект антивирусов. В терминологии MITRE это Software Packing (T1027.002, тактика Defense Evasion).

Как определить упаковку в Ghidra:

Энтропия секций. Энтропия — мера «хаотичности» данных. У обычного скомпилированного кода она в районе 5.5–6.5, у зашифрованных или сжатых данных — выше 7.0. В Ghidra: Window → Entropy, смотрите секцию .text. Значения выше 7.0 — почти гарантия, что секция зашифрована.

Количество импортов. Упакованный бинарь импортирует минимум WinAPI-функций — часто лишь LoadLibraryA, GetProcAddress, VirtualAlloc, VirtualProtect. Проверьте через Window → Symbol Table → Imports. Если импортов меньше десяти, а файл весит сотни килобайт — перед вами пакер.

Нечитаемые строки. Search → Strings. В упакованном образце увидите мусор или единичные строки от самого stub-кода (распаковщика) — никаких URL, путей к файлам или сообщений об ошибках.

Если бинарь упакован, путь два: найти OEP (Original Entry Point — точку, где пакер передаёт управление распакованному коду) по характерному jmp в конце stub-секции, или дампить распакованный образ из памяти в отладчике. Второй путь надёжнее — об этом ниже, в разделе про динамику.

Антиотладка в малвари: проверки PEB и отладочных флагов

Самые частые anti-debugging техники (Debugger Evasion, T1622) сводятся к чтению системных структур Windows. Lumma использует несколько проверок, и большинство видны в статике.

PEB.BeingDebugged. PEB (Process Environment Block) — структура в памяти Windows с информацией о текущем процессе. Поле BeingDebugged по смещению +2 равно единице, если процесс запущен под отладчиком. В 32-битном коде доступ к PEB идёт через сегментный регистр: fs:[0x30] — указатель на PEB, дальше чтение байта по смещению +2.

В декомпиляторе Ghidra паттерн выглядит так:

// Ghidra decompiler — типичная проверка BeingDebugged
peb = *(int *)(__readfsdword(0x30));
if (*(byte *)(peb + 2) != 0) {
    ExitProcess(0);  // завершение при детекте отладчика
}

Как найти: Search → Memory, ищите байтовую последовательность 64 A1 30 00 00 00 (инструкция mov eax, fs:[0x30] для 32-бит). Пройдите по каждому xref (cross-reference — перекрёстная ссылка, показывающая, откуда в коде вызывается адрес или функция): если после чтения PEB следует условный переход на ExitProcess или TerminateProcess — это антиотладка. В одном семпле Lumma таких проверок обычно от двух до пяти, раскиданных по разным функциям.

NtQueryInformationProcess. Вызов этой недокументированной WinAPI-функции с параметром ProcessDebugPort (значение 7) возвращает ненулевое значение, если процесс под отладчиком. В статике ловится через поиск строки NtQueryInformationProcess в Import Table. Но в свежих билдах Lumma вызов часто скрыт за динамическим разрешением API — и тут статика упирается в стену.

NtGlobalFlag и Heap Flags. Дополнительные проверки: значение NtGlobalFlag в PEB (смещение +0x68 в 32-бит) и флаги хипа. Под отладчиком NtGlobalFlag содержит комбинацию флагов FLG_HEAP_ENABLE_TAIL_CHECK | FLG_HEAP_ENABLE_FREE_CHECK | FLG_HEAP_VALIDATE_PARAMETERS (значение 0x70). В Ghidra ищите чтение PEB по тому же fs:[0x30] с последующим доступом к смещению 0x68 и сравнением с 0x70.

Обфускация кода и динамическое разрешение API

Без расшифровки строк вы не получите IOC — C2-домены, пути для кражи файлов, ключи реестра для закрепления. Разработчики Lumma применяют несколько уровней обфускации.

Stack strings (строки на стеке). Каждый символ строки записывается отдельной инструкцией mov byte [ebp-X], 0xNN. В декомпиляторе Ghidra это серия присваиваний: local_20 = 0x68; local_1f = 0x74; local_1e = 0x74; local_1d = 0x70; — побайтовая сборка строки http. Ghidra не соберёт из этих операций читаемую строку автоматически. Нужно или вручную перевести hex-значения в символы, или написать скрипт.

XOR-шифрование. Зашифрованные данные хранятся в секции .data или .rdata, а функция-декриптор применяет XOR с ключом фиксированной длины. По данным анализа eSentry, Lumma активно использует XOR для обфускации конфигурационных данных и payload’ов. В Ghidra ищите функции с циклом, внутри которого есть операция XOR над буфером:

// Типичный XOR-декриптор Lumma (Ghidra decompiler, упрощённо)
for (i = 0; i < buf_len; i++) {
    encrypted_buf[i] = encrypted_buf[i] ^ key[i % key_len];
}
// После цикла encrypted_buf содержит расшифрованную строку

Деобфускация малвари (Deobfuscate/Decode Files or Information, T1140) — без этого этапа анализ стоит на месте. Для XOR-строк можно написать Ghidra-скрипт на Python (Jython), который автоматически расшифрует все найденные буферы, ориентируясь на паттерн вызова декриптора. Задача уровня «написал один раз — применяешь к каждому новому билду».

Dynamic API Resolution (T1027.007). Вместо явного импорта WinAPI-функций через Import Table Lumma вычисляет хэши имён функций в рантайме и резолвит их через GetProcAddress или ручной обход Export Table DLL. В Ghidra это выглядит как вызов по адресу из переменной: call dword ptr [ebp-0x1C] вместо привычного call IsDebuggerPresent.

Что можно сделать в статике: найти хэш-функцию (обычно короткий цикл с операциями ror/add над каждым символом имени), вычислить хэши известных WinAPI-имён и промаппить их на вызовы. Есть готовые базы хэшей (например, HashDB). Но если хэш-алгоритм кастомный или использует salt — придётся вычислять руками или переключаться на отладчик.

Техники антианализа малвари, которые требуют отладчика

Есть класс техник, которые в дизассемблере выглядят как безобидный код или вообще скрыты под слоем пакера. Ниже — то, что проявляется только при реальном исполнении.

Sandbox evasion: системные проверки и детект пользователя

Lumma проверяет, запущен ли он в песочнице (sandbox — изолированная среда для безопасного запуска и изучения малвари) или на реальной машине жертвы. Проверки идут на трёх уровнях.

System Checks (T1497.001 — проверки системных параметров, подтехника Virtualization/Sandbox Evasion). Lumma через инструкцию cpuid читает информацию о процессоре: если гипервизор-бит выставлен (leaf 1, ECX bit 31) — это виртуальная машина. Также проверяет MAC-адрес сетевого адаптера на принадлежность VMware (начинается с 00:0C:29) или VirtualBox (08:00:27). Может считать количество ядер CPU и объём RAM: менее двух ядер или менее 4 ГБ — подозрительная среда.

В Ghidra вы увидите инструкцию cpuid и последующие побитовые сравнения. Но реальные значения регистров (EAX, EBX, ECX, EDX) приходят только в рантайме — статика покажет логику проверки, но не скажет, пройдёт ли семпл её на вашей конкретной машине.

User Activity Based Checks (T1497.002 — проверки активности пользователя). Вот тут начинается самое интересное. По данным исследования Outpost24, в LummaC2 v4.0 стилер отслеживал перемещения мыши и применял тригонометрические вычисления для определения, двигает ли мышью живой человек или автоматический скрипт песочницы. Малварь вычисляла углы между последовательными позициями курсора — у человека траектория хаотичная, у автоматизации — линейная или циклическая.

В Ghidra можно найти вызовы GetCursorPos и математические функции (atan2, вычисления с плавающей точкой), но без запуска и живого ввода мыши не подтвердить, что именно эта проверка блокирует выполнение основного payload’а.

Time Based Checks (T1497.003 — проверки по времени). Классический приём: вызов GetTickCount или QueryPerformanceCounter до и после блока кода. Если разница слишком маленькая — инструкции выполнились мгновенно, что типично для трассировки в отладчике. Если слишком большая — характерно для пошаговой отладки. В статике видны два вызова timing-функции и сравнение результата, но пороговые значения (конкретные миллисекунды) становятся понятны только в динамике.

Динамический анализ вредоносного ПО: process hollowing и runtime-декрипт

По данным анализа Qualys Threat Research Unit, Lumma использует process hollowing. Суть техники: легитимный процесс Windows запускается в приостановленном состоянии (CREATE_SUSPENDED), его память полностью заменяется вредоносным кодом, после чего процесс возобновляется и выполняет payload от имени доверенного процесса. В исследованных семплах целевым процессом выступал BitLockerToGo.exe — штатная утилита Windows для шифрования дисков. Выбор неслучайный: доверенный системный процесс не вызывает подозрений у антивируса.

В статике видна характерная цепочка WinAPI-вызовов: CreateProcessW (создание процесса) → NtUnmapViewOfSection (выгрузка исходного кода) → VirtualAllocEx (выделение памяти) → WriteProcessMemory (запись payload’а) → ResumeThread (возобновление выполнения). Но сам payload, который записывается в процесс-жертву, расшифровывается только в рантайме — его содержимое покажет исключительно отладчик или дамп памяти процесса.

Аналогично с финальной стадией загрузки: в исследованных Qualys образцах PowerShell-скрипт-загрузчик содержал AES-зашифрованный payload с hardcoded-ключом дешифрования. Ключ виден в статике (он просто лежит в скрипте), но расшифровка, запись в память и инъекция происходят только при реальном исполнении. Кроме того, Lumma проверяет наличие антивирусных процессов (wrsa.exe — Webroot, opssvc.exe — Quick Heal, bdservicehost.exe — Bitdefender) через tasklist и findstr, адаптируя поведение под обнаруженную защиту.

Реверс инжиниринг Lumma Stealer: пошаговый workflow

Ниже — практический порядок действий для реверса свежего семпла. Что понадобится: Ghidra 11+ (работает на Windows, Linux, macOS, нужна JDK 17+); для динамической части — Windows VM с отключённым Windows Defender (или с настроенным исключением для рабочей директории), x64dbg или x32dbg с плагином ScyllaHide (скрывает отладчик от PEB-проверок).

Шаг 1. Получить хэш и подтвердить семпл. Посчитайте SHA-256 файла. В PowerShell: Get-FileHash .\sample.exe -Algorithm SHA256. В Linux: sha256sum sample.exe. Проверьте хэш на VirusTotal. Ожидаемый результат: 50+ движков детектируют файл. Если VirusTotal показывает detection rate выше 40 и маркирует как Lumma/LummaC2/Trojan.Stealer — подтверждение, что образец тот самый.

Шаг 2. Загрузить в Ghidra и оценить упаковку. Создайте проект (File → New Project), импортируйте файл (File → Import File). Ghidra определит формат (PE32 или PE32+) и предложит автоанализ — соглашайтесь и ждите завершения. Проверьте энтропию секции .text (Window → Entropy) и количество импортов. Если энтропия выше 7.0, а импортов меньше 10 — бинарь упакован, переходите к шагу 3. Если нет — сразу к шагу 4.

Шаг 3. Дамп распакованного образа (если упакован). Перенесите семпл в Windows VM. Откройте в x32dbg/x64dbg. Включите ScyllaHide (Plugins → ScyllaHide → Options → выберите профиль, например «VMProtect» — покрывает большинство проверок). Поставьте breakpoint (точку останова, где отладчик приостановит выполнение) на VirtualProtect — пакер обычно вызывает её для снятия защиты записи перед распаковкой. Пройдите несколько итераций (F9 — продолжить выполнение). Когда пакер передаст управление на OEP, сделайте дамп через Scylla (плагин x64dbg) → IAT Autosearch → Dump. Загрузите дампнутый файл в Ghidra заново. Ожидаемый результат: в дампе энтропия .text ниже 6.5, импортов десятки, строки читаемые.

Шаг 4. Найти и задокументировать антиотладочные проверки. В Ghidra: Search → Memory → Hex: 64 A1 30 00 00 00 (PEB access для 32-бит). Пройдите по каждому xref, проверьте наличие условного перехода на ExitProcess. Также проверьте Import Table на IsDebuggerPresent, NtQueryInformationProcess, CheckRemoteDebuggerPresent. Запишите адреса всех антиотладочных проверок — в отладчике их можно обойти, запатчив условный переход на NOP или инвертировав условие. Результат: список из 2–5 адресов с проверками и понимание, какой тип проверки используется.

Шаг 5. Извлечь строки. Найдите функцию-декриптор (цикл с XOR) через Listing → правый клик → References → Find References To. Выпишите ключ XOR и длину. Примените к буферам, на которые ссылается декриптор. Ожидаемый результат: расшифрованные C2-домены (для Lumma характерен TLD .shop) и пути к файлам, которые стилер ищет на машине жертвы (*seed*.txt, *wallet*.txt, *ledger*.txt и др.).

Шаг 6. Промаппить техники на MITRE ATT&CK. Каждую найденную технику фиксируйте с T-кодом: антиотладка → T1622, упаковка → T1027.002, sandbox evasion → T1497 (с подтехниками .001/.002/.003), динамический API → T1027.007, деобфускация → T1140. Это формирует стандартизированный профиль угрозы, читаемый любым аналитиком в любом SOC.

Сводная таблица: статика vs динамика в реверсе малвари

Техника антианализа T-код MITRE Видна в статике (Ghidra) Требует отладчика
PEB.BeingDebugged T1622 Да — паттерн fs:[0x30]+2 Нет, если импорт явный
NtQueryInformationProcess T1622 Частично — зависит от dynAPI Да, при динамическом API
Software Packing T1027.002 Да — энтропия, малый import Да — для дампа из памяти
XOR-строки T1140 Да — декриптор виден в CFG Частично — скрипт или отладчик
Dynamic API Resolution T1027.007 Частично — хэш-функция видна Да — для маппинга хэшей
CPUID / VM detection T1497.001 Частично — инструкция видна Да — нужны значения регистров
Тригонометрия мыши T1497.002 Частично — GetCursorPos+math Да — нужен реальный ввод
Timing checks T1497.003 Частично — два вызова timer Да — для пороговых значений
Process hollowing — Частично — цепочка API Да — для содержимого payload

Главный вывод из таблицы: Ghidra статический анализ даёт карту антианализа — какие техники применены и где именно в коде. Отладчик даёт реальные значения и payload’ы. Оптимальный workflow: сначала статика для разведки, потом точечная динамика для подтверждения и извлечения IOC.

Отдельное наблюдение, которое стоит проговорить. Разработчики Lumma обновляют пакер и ротируют C2 значительно чаще, чем меняют логику антианализа. PEB-проверки, timing-трюки, тригонометрия мыши — всё это кочует из билда в билд с минимальными изменениями. Один раз разобрав поведенческий скелет Lumma в Ghidra и написав скрипты для автоматической деобфускации строк, вы получаете инструмент, работающий на десятках последующих семплов. Несколько часов на первый разбор — минуты на каждый следующий.

Я считаю, что статический анализ для реверса стилеров категорически недооценён в русскоязычном комьюнити. Большинство разборов Lumma в рунете сводятся к описанию цепочки заражения — фейковая CAPTCHA, PowerShell, payload — и не заходят дальше поверхностного анализа в песочнице. Между тем именно понимание внутренней механики антианализа отличает аналитика, чьи YARA-правила ломаются на каждом втором билде, от того, чьи правила живут месяцами. Чтение дизассемблера, навигация по CFG (Control Flow Graph — граф потока управления, визуализация ветвлений кода), написание скриптов деобфускации — фундамент, без которого Ghidra остаётся просто красивым дизассемблером. Если хотите выстроить эту базу системно, а не ковыряться наугад — на IB Basics на codeby.school фундамент закрывается за пару месяцев, после чего можно садиться за Ghidra осознанно.

Эту тему и смежные навыки разбирают на практике в курсе «Профессия Реверс-инженер» Codeby Academy.