Отладка шелл-кода в x64dbg: загрузка PIC-стаба, точка останова на VirtualAlloc и распаковка в памяти

PE-файл весом 47 КБ, в таблице импорта — ровно две функции: VirtualAlloc и VirtualProtect. Энтропия секции .data упирается в потолок. Если вы видите такую картину впервые — перед вами портрет лоадера. Он прячет настоящий payload внутри себя и расшифровывает его только в рантайме. По данным исследования Embee Research, именно так устроены загрузчики Cobalt Strike: шелл-код декодируется XOR-циклом с 4-байтным ключом и записывается в буфер, созданный через VirtualAlloc. Статический анализ здесь бессилен — IDA или Ghidra покажут зашифрованный блоб, а не реальные инструкции. Чтобы достать расшифрованный шелл-код, нужно поставить точку останова в x64dbg и поймать момент записи в память. Ниже — полный пошаговый разбор: от открытия образца до сохранённого дампа.
Почему шелл-код нельзя просто открыть в отладчике
Шелл-код — фрагмент машинных инструкций, который работает как PIC (Position-Independent Code, позиционно-независимый код). В отличие от обычного .exe, у него нет PE-заголовков (PE — Portable Executable, стандартный формат исполняемых файлов Windows), нет таблицы импорта, нет секций. Он рассчитан на выполнение с произвольного адреса в памяти — и в этом вся проблема.
x64dbg на входе ожидает валидный PE-файл — с сигнатурой MZ, точкой входа и описанием секций. Загрузите сырой .bin с шелл-кодом — отладчик не поймёт, откуда начинать выполнение, и откажется работать.
В реальных атаках шелл-код попадает в память через лоадер (дроппер, стейджер) — небольшой исполняемый файл, который делает три вещи:
- Выделяет блок памяти через WinAPI-функцию VirtualAlloc с правами на выполнение
- Расшифровывает или распаковывает шелл-код в этот блок
- Передаёт управление на первый байт буфера — через CreateThread, прямой
callилиjmp
Наша задача — перехватить промежуток между шагами 2 и 3: шелл-код уже расшифрован, но ещё не начал выполняться. Именно в этот момент его можно сдампить из памяти в файл.
Место шелл-кода в цепочке атаки
MITRE ATT&CK — открытая база тактик и техник атак. T-коды (T1027, T1055 и т.п.) — уникальные идентификаторы техник. Если вы только начинаете работать с ATT&CK, воспринимайте их как «номера статей в уголовном кодексе для малвари» — по ним аналитики классифицируют поведение вредоноса. Шелл-код в составе лоадера задействует сразу несколько техник:
- Software Packing (T1027.002) — payload зашифрован или сжат внутри лоадера, чтобы обойти сигнатурный анализ антивирусов
- Deobfuscate/Decode Files or Information (T1140) — лоадер расшифровывает шелл-код в рантайме (XOR-цикл, AES, RC4)
- Native API (T1106) — шелл-код напрямую вызывает WinAPI-функции вместо высокоуровневых обёрток
- Reflective Code Loading (T1620) — шелл-код загружается и выполняется целиком в памяти, без записи на диск
- Dynamic API Resolution (T1027.007) — шелл-код находит адреса WinAPI-функций в рантайме, обходя PEB (Process Environment Block — структура, содержащая информацию о загруженных модулях процесса)
Типичная цепочка выглядит так: фишинговое вложение (Initial Access) запускает лоадер → лоадер вызывает VirtualAlloc → расшифровывает шелл-код в буфер → шелл-код устанавливает соединение с C2-сервером (Command and Control — сервер управления, через который атакующий взаимодействует с заражённой машиной). Наша точка перехвата — между «вызывает VirtualAlloc» и «шелл-код начинает исполняться». Если лоадер выполняет также PE-инъекцию в другой процесс (T1055.002 — Portable Executable Injection), шелл-код может быть промежуточным звеном перед внедрением полноценного PE-модуля.
Зачем это знать: когда вы видите в отчёте песочницы строку «T1027.002 + T1140 + T1106» — вы уже понимаете, что внутри лоадер с зашифрованным шелл-кодом, и знаете, где ставить брейкпоинт.
Требования к окружению для динамического анализа вредоносного кода
| Параметр | Минимум | Рекомендуется |
|---|---|---|
| ОС | Windows 10 x64 (виртуальная машина) | Windows 10 22H2 x64 с FlareVM |
| RAM виртуальной машины | 4 ГБ | 8 ГБ |
| x64dbg | Актуальный snapshot с github.com/x64dbg/x64dbg (проект активен, коммиты еженедельно) | + плагины ScyllaHide, xAnalyzer |
| Сеть | Отключена полностью | Host-only adapter для контролируемого C2-трафика |
| Антивирус | Windows Defender Real-time protection: Off | Папка с образцами в исключениях |
| Снимок VM | Обязателен до запуска образца | Один snapshot «чистый» + один «с загруженным образцом» |
FlareVM — преднастроенная среда от Mandiant для анализа вредоносного ПО; включает x64dbg, Ghidra, Python, утилиты для работы с PE-файлами. Если FlareVM нет — хватит чистой Windows 10 в VirtualBox/VMware с установленным x64dbg.
Никогда не запускайте вредоносные образцы на хостовой ОС. Виртуальная машина с отключённой сетью и заранее сделанным snapshot — не рекомендация, а обязательное условие. Один случайный клик без snapshot — и придётся переставлять систему.
Загрузка позиционно-независимого кода в x64dbg
Два рабочих подхода. Выбор зависит от того, в каком виде к вам попал шелл-код.
Конвертация сырых байт в PE через shellcode2exe
Если шелл-код лежит как отдельный .bin-файл (извлечённый из трафика, из макроса, из памяти другого процесса), его нужно обернуть в минимальную PE-структуру. Утилита shellcode2exe (автор Mario Vilas) добавляет заголовок MZ, одну секцию с правами RWX и точку входа на первый байт шелл-кода. Согласно описанию на SecurityBreak.io, команда выглядит так:
shellcode2exe.exe payload.bin output.exe
Результат открывается в x64dbg как обычный .exe. Отладчик остановится на системной точке останова; после нажатия F9 выполнение дойдёт до Entry Breakpoint — это первый байт вашего шелл-кода.
PE-stub лоадер: шелл-код зашит внутри бинаря
Если шелл-код упакован внутри PE-файла (лоадера), конвертация не нужна — загружаем в x64dbg сам лоадер. Задача меняется: перехватить момент, когда лоадер выделит память и запишет туда расшифрованный payload.
| Подход | Когда использовать | Когда не использовать |
|---|---|---|
| shellcode2exe | Шелл-код в виде сырого .bin/.raw | Шелл-код зашит внутри PE-лоадера |
| Загрузка PE-лоадера | Шелл-код распаковывается из исполняемого файла | Нет PE-файла, только сырые байты |
| Собственный лоадер на C | Нужен контроль над параметрами VirtualAlloc | Быстрый анализ без компиляции |
VirtualAlloc: точка останова и пошаговый x64dbg shellcode анализ
Главный практический блок. Разбираем каждый шаг на примере PE-лоадера с зашифрованным шелл-кодом.
Шаг 1. Открываем образец и доходим до точки входа.
Запускаем x64dbg (для 64-битных образцов — x64dbg.exe, для 32-битных — x32dbg.exe). File → Open → выбираем лоадер. Отладчик остановится на System Breakpoint — служебной точке внутри ntdll.dll. Нажимаем F9 (Run) — выполнение продолжится до Entry Breakpoint. Ожидаемый результат: в окне дизассемблера (верхняя левая панель) видна первая инструкция лоадера — обычно sub rsp, 28h или аналогичный пролог функции.
Шаг 2. Ставим точку останова на VirtualAlloc.
В командной строке x64dbg (нижняя панель) вводим bp VirtualAlloc. Если лоадер использует также VirtualProtect для смены прав памяти на RWX, добавляем bp VirtualProtect. В окне Breakpoints появится строка с адресом функции внутри kernelbase.dll — подтверждение, что брейкпоинт установлен. Почему именно VirtualAlloc: это основная WinAPI-функция для выделения памяти. Лоадеры вызывают её перед записью расшифрованного payload — других вариантов для «честных» лоадеров почти нет.
Шаг 3. Запускаем до срабатывания. F9. Выполнение продолжится до первого вызова VirtualAlloc. Отладчик остановится на входе в функцию. RIP (регистр-указатель на текущую инструкцию; аналог EIP для 64-битного режима) указывает на первую инструкцию VirtualAlloc. В окне регистров (правая панель) смотрим параметры вызова по 64-битному соглашению:
- RCX — базовый адрес (обычно 0, «система выберет сама»)
- RDX — размер выделяемого блока (например, 0x10000 = 64 КБ)
- R8 — тип выделения (MEM_COMMIT = 0x1000)
- R9 — права доступа
Значение R9 = 0x40 означает PAGE_EXECUTE_READWRITE (RWX — Read, Write, Execute). Запомните эту цифру. Лоадер запрашивает память с правами на запись и выполнение одновременно. Легитимный софт так почти не делает — это красный флаг.
Шаг 4. Execute Until Return — получаем адрес буфера.
Ctrl+F9 (Execute Until Return). Функция VirtualAlloc отработает целиком, но управление не уйдёт дальше. Затем F8 (Step Over) для выполнения инструкции ret. Ожидаемый результат: регистр RAX (в 64-битном режиме функции возвращают результат именно в RAX) содержит адрес начала выделенного буфера. Запишите этот адрес — сюда лоадер запишет расшифрованный шелл-код.
Шаг 5. Переходим к содержимому буфера. Правой кнопкой по значению RAX в окне регистров — Follow in Dump. В нижнем левом окне (Hex Dump) откроется содержимое буфера. Сейчас там сплошные нули — память выделена, но ещё пуста. Это нормально.
Аппаратные точки останова: трассировка распаковки шелл-кода в памяти
Теперь нужно отследить момент, когда лоадер запишет расшифрованные данные в буфер. Для этого используем аппаратные точки останова (hardware breakpoints).
Шаг 6. Аппаратный брейкпоинт на первый байт.
В окне Dump: правая кнопка по первому (нулевому) байту буфера → Breakpoint → Hardware → Access → Byte. Почему аппаратный, а не обычный программный: программный брейкпоинт записывает байт 0xCC (инструкция int3) в память. Это может сломать логику лоадера или быть обнаружено антиотладочным кодом. Аппаратный использует отладочные регистры процессора (DR0–DR3) и не трогает содержимое памяти вообще. Минус — таких регистров всего четыре, но для нашей задачи одного хватит.
Шаг 7. Ловим момент записи.
F9. Лоадер продолжит работу: расшифрует payload и начнёт записывать байты в буфер. Как только первый байт будет записан, сработает аппаратный брейкпоинт. Ожидаемый результат: в Dump-окне видно, что первый байт буфера изменился. По данным Embee Research, для шелл-кода типичный первый байт — 0xFC (инструкция cld, Clear Direction Flag). Если на месте нуля появился FC — с высокой вероятностью вы поймали шелл-код.
Шаг 8. Заполняем буфер целиком.
Снимаем аппаратный брейкпоинт (окно Breakpoints → удалить hardware breakpoint) и нажимаем Ctrl+F9 (Execute Until Return). Текущая функция расшифровки завершится, заполнив буфер. Ожидаемый результат: Dump-окно показывает данные, которые уже не выглядят случайным мусором. Если шелл-код загружает C2-агент, в буфере могут быть видны ASCII-строки — URL-пути, User-Agent, имена библиотек типа wininet.
Дамп и валидация: анализ шелл-кода вручную
Шаг 9. Проверяем валидность кода. В Dump-окне: правой кнопкой по первому байту → Follow in Disassembler. x64dbg попытается дизассемблировать байты как машинный код.
Признаки валидного шелл-кода:
— Инструкции осмысленные — mov, push, call, jmp, а не повторяющийся мусор типа add byte ptr [rax], al
— Есть вызовы функций (call) и циклы (loop, jne)
— Нет длинных последовательностей nop или int3
Если дизассемблер показывает нормальный код — вы нашли распакованный шелл-код.
Шаг 10. Сохраняем дамп.
Правая кнопка по первому байту в Dump → Follow in Memory Map. В карте памяти находим блок с правами ERW или RWX, правая кнопка → Dump Memory to File. Сохраняем как unpacked_shellcode.bin. Этот файл можно загрузить в Ghidra или IDA Free как Raw Binary для статического анализа, проверить YARA-правилами на известные C2-фреймворки, или эмулировать через Speakeasy (инструмент от Mandiant для эмуляции шелл-кода без реального выполнения).
Ограничения: когда VirtualAlloc-брейкпоинт не работает
Описанный подход покрывает простые и среднесложные лоадеры. Вот что может его сломать — и что с этим делать.
Антиотладочные техники (T1622 — Debugger Evasion)
Лоадер проверяет наличие отладчика:
— Вызов IsDebuggerPresent() — возвращает TRUE, если процесс под отладкой
— Прямое чтение PEB.BeingDebugged (флаг в структуре процесса)
— Замер времени через RDTSC — под отладкой инструкции выполняются заметно медленнее
Решение: плагин ScyllaHide для x64dbg скрывает присутствие отладчика, подменяя результаты этих проверок. Ставится через менеджер плагинов x64dbg. На практике закрывает 80% антиотладочных трюков без дополнительной настройки.
Обход песочниц (T1497 — Virtualization/Sandbox Evasion)
Лоадер отказывается работать в виртуальной машине: — Проверка MAC-адреса на принадлежность VMware/VirtualBox — Поиск процессов vmtoolsd.exe, VBoxService.exe — Проверка числа ядер CPU и объёма RAM — менее 2 ядер и 4 ГБ вызывают подозрение
Решение: убрать Guest Additions, сменить MAC-адрес на случайный, выделить 4+ ядра и 8 ГБ RAM. Некоторые лоадеры проверяют ещё и разрешение экрана (менее 1024×768 = песочница), так что полноэкранный режим VM тоже не помешает.
Многоступенчатая распаковка
Некоторые лоадеры вызывают VirtualAlloc десятки раз — для промежуточных буферов, строк, конфигурации. Брейкпоинт сработает на каждом вызове, и придётся вручную определять, какой буфер содержит финальный payload.
Подсказка: смотрите на размер в RDX. Блок в 4 КБ — скорее промежуточный буфер для строк или ключей. Блок в 50–500 КБ с правами RWX — более вероятный кандидат на payload. Ещё один маркер: если после записи в буфер лоадер вызывает CreateThread с адресом начала этого буфера — вы нашли финальный шелл-код.
Альтернативные API
Продвинутые лоадеры используют NtAllocateVirtualMemory (низкоуровневый ntdll-эквивалент VirtualAlloc) или маппят секции через NtMapViewOfSection. В таком случае ставим bp NtAllocateVirtualMemory вместо VirtualAlloc. Логика анализа остаётся той же: перехватить возврат адреса буфера, поставить аппаратный брейкпоинт, дождаться заполнения.
Чеклист: пошаговый алгоритм отладки шелл-кода в x64dbg
- Подготовить изолированную VM (Windows 10 x64, x64dbg, сеть отключена, snapshot сделан)
- Открыть образец в x64dbg, дойти до Entry Breakpoint (F9)
- Установить
bp VirtualAllocиbp VirtualProtect - Запустить (F9), дождаться срабатывания на VirtualAlloc
- Проверить R9 на значение 0x40 (PAGE_EXECUTE_READWRITE)
- Ctrl+F9 (Execute Until Return), затем F8 (Step Over) — адрес буфера в RAX
- Follow in Dump — убедиться, что буфер пуст
- Аппаратный брейкпоинт: Breakpoint → Hardware → Access → Byte на первый байт
- F9 — дождаться записи в буфер (ожидаемый первый байт: 0xFC)
- Снять hardware breakpoint, Ctrl+F9 — дать буферу заполниться
- Follow in Disassembler — проверить валидность кода
- Follow in Memory Map → Dump Memory to File — сохранить для дальнейшего анализа
Автоматические песочницы стали настолько удобными, что часть аналитиков перестала открывать отладчик вообще. Загрузил файл, подождал три минуты, получил отчёт с C2-адресами. Работает — пока лоадер не содержит таймер-бомбу, проверку окружения или многоступенчатую распаковку. На каждый второй такой образец песочница выдаёт чистый вердикт, потому что шелл-код просто не распаковался. Ручная отладка через VirtualAlloc-брейкпоинт — не ретро-подход из эпохи OllyDbg. Это способ увидеть, что происходит внутри лоадера, когда он решает, стоит ли вообще распаковываться. Аппаратный брейкпоинт на буфер не обмануть вызовом IsDebuggerPresent — он работает на уровне процессорных регистров DR0–DR3, а не через модификацию кода. Пара «VirtualAlloc breakpoint + hardware watchpoint на буфер» покрывает большинство лоадеров, которые попадают на стол аналитику. Кто освоил эту связку — тому следующий уровень (многоступенчатая распаковка, обфускация потока управления, ntdll-syscalls) даётся не как потрясение, а как естественное усложнение. Формула на бумаге понятна, но распаковка по-настоящему ощущается только когда сам ставишь брейкпоинт и видишь, как нули в дампе превращаются в живой код; готовый стенд с задачами такого типа есть на HackerLab.pro — российской CTF-платформе экосистемы Codeby с категориями reverse и forensics, где можно отработать навык без риска на реальных образцах.
Эту тему и смежные навыки разбирают на практике в курсе «Профессия Реверс-инженер» Codeby Academy.