Infostealers - чемпионы 2024 года: 32% обнаруженных семейств по IBM X-Force Threat Intelligence Index 2025, ransomware позади. В даркнет ежедневно сливается больше 6000 свежих наборов учётных данных. Растущая доля этого потока - Python-based стилеры, упакованные через PyInstaller: BlankGrabber, XillenStealer и десятки их форков расходятся через Discord-каналы, поддельные GitHub-репозитории и «крякнутый» софт. Русскоязычные гайды по анализу малвари заканчиваются на
strings и VirusTotal - без разбора PyInstaller, без восстановления байткода, без извлечения C2 из обфусцированных строк. Здесь - полный цикл анализа вредоносного ПО на Python, от бинаря до IoC. Вскрываем, препарируем, достаём адреса.Pyinstaller - это популярный инструмент для упаковки Python-приложений в standalone-исполняемые файлы.
Бизнес-логика: зачем атакующему Python-стилер и где он в kill chain
Прежде чем лезть в байткод - контекст. Откуда берутся эти образцы и как встраиваются в цепочку атаки.Типичная цепочка доставки: жертва скачивает «кряк» или утилиту с GitHub/Discord (initial access). Внутри - PyInstaller-бинарь, при запуске распаковывается Python-скрипт стилера (execution). Скрипт тянет пароли из браузеров Chromium и Firefox, токены Discord, кошельки криптовалют, cookie-файлы и скриншоты (collection). Собранное архивируется и улетает на C2 - обычно через Telegram Bot API или HTTP POST (exfiltration). Украденные credential-пакеты продаются на маркетплейсах Initial Access Broker или идут в ход для дальнейшего проникновения в инфраструктуру жертвы.
Почему Python? Порог входа - на уровне плинтуса. XillenStealer, по данным CYFIRMA, создан разработчиком, который представляется 15-летним подростком. Модульная архитектура: каждая функция (сбор из браузеров, кошельки, скриншоты) живёт в отдельном скрипте. PyInstaller превращает набор .py-файлов в standalone-exe без зависимости от установленного Python на машине жертвы.
С точки зрения MITRE ATT&CK, Python-стилеры реализуют характерный набор техник:
- Obfuscated Files or Information (T1027, Stealth) - многослойная обфускация кода и шифрование payload
- System Checks (T1497.001, Stealth / Discovery) - проверки на виртуальную машину и sandbox перед выполнением основного кода
- Gather Victim Identity Information (T1589, Reconnaissance) - сбор IP, геолокации и сетевой информации через внешние сервисы
Идентификация PyInstaller-бинаря
Требования к окружению
Все дальнейшие шаги - исключительно в изолированной среде. Запускать малварь на рабочей машине - идея уровня «а давайте проверим, работает ли огнетушитель, подпалив серверную».- ОС: Windows 10/11 или GNU/Linux (Ubuntu 22.04+)
- RAM: минимум 4 ГБ для статического triage, 8 ГБ для sandbox с динамическим анализом
- Python: 3.8–3.12, версия должна совпадать с версией анализируемого образца
- Triage: Detect It Easy (DiE), утилита
strings(GNU или Sysinternals) - Распаковка: pyinstxtractor (GitHub: extremecoders-re/pyinstxtractor, активно поддерживается)
- Декомпиляция: uncompyle6 (Python 3.8 и ниже), pycdc / Decompyle++ (Python 3.9+)
- Sandbox: изолированная VM с FakeNet-NG (Mandiant FLARE) и Wireshark
- RE нативных компонентов: radare2 6.1.7+ или Ghidra (NSA, активно поддерживается)
Маркеры PyInstaller в образце
Вопреки распространённому совету искать строкуPYZ-00.pyz - имя PYZ-архива гуляет между билдами и конфигурациями PyInstaller. Есть маркеры понадёжнее:MEIPASS- переменная окружения, через которую PyInstaller указывает путь к распакованным ресурсам при runtime- Префикс
pyi-в именах временных файлов и процессов - Модули
pyimod02_importersиpyimod03_ctypes- служебные компоненты PyInstaller runtime, сидят в каждом бинаре - Паттерн
_MEIXXXXXXв именах временных директорий
strings -a suspect.exe | grep -iE "MEIPASS|pyimod|pyi-|_MEI". Есть совпадения - PyInstaller-упаковка подтверждена.Если строковый поиск ничего не дал (маркеры замаскированы агрессивным пакером поверх PyInstaller), смотрим энтропию. PyInstaller-бинари содержат сжатый архив в overlay-секции PE-файла с энтропией выше 7.5 из 8.0. В Detect It Easy это видно как характерный блок высокой плотности в конце файла на графике Entropy.
[Применимо: triage любых PE-образцов при первичном анализе]
Ограничение: маркеры специфичны для PyInstaller. Бинари Nuitka (компиляция Python в C) и cx_Freeze - другие индикаторы, другие инструменты распаковки. Если
strings не нашёл PyInstaller-маркеров при высокой энтропии - проверяйте альтернативные упаковщики.Распаковка и восстановление .pyc-байткода
📚 Часть контента скрыта. Этот материал доступен участникам сообщества с рангом One Level или выше
Получить доступ просто — достаточно зарегистрироваться и проявить активность на форуме
Получить доступ просто — достаточно зарегистрироваться и проявить активность на форуме
Скрипт восстановления с ветками для обоих форматов:
Python:
import struct
MAGIC = {'3.8': 3413, '3.9': 3425, '3.10': 3439, '3.11': 3495, '3.12': 3531}
def restore_pyc(infile, outfile, ver='3.10'):
magic = struct.pack('<H', MAGIC[ver]) + b'\r\n' # <H (2 байта LE) + \r\n = стандартный 4-байтовый magic CPython
# Python 3.3-3.6: magic + 8 нулей (итого 12 байт заголовка)
# Python 3.7+: magic + 12 нулей (итого 16 байт заголовка)
pad = b'\x00' * (8 if ver in ('3.3','3.4','3.5','3.6') else 12)
with open(infile, 'rb') as f:
data = f.read()
with open(outfile, 'wb') as f:
f.write(magic + pad + data)
python310.dll - значит ver='3.10' и magic number 3439.Декомпиляция и её ограничения
После восстановления заголовка - декомпиляция:- Python ≤3.8:
uncompyle6 restored.pyc > output.py. Работает стабильно для этих версий, но проект архивирован и обновлений не получает - Python 3.9+:
pycdc restored.pyc > output.py. Decompyle++ (C++-декомпилятор), частично поддерживает 3.9–3.12
:=, match/case из 3.10+, вложенные comprehensions) могут быть восстановлены криво или пропущены. Для критичных фрагментов - работа с сырым байткодом через модуль dis:python -c "import dis, marshal; f=open('restored.pyc','rb'); f.read(16); dis.dis(marshal.loads(f.read()))"Команда применима только к
restored.pyc после восстановления заголовка скриптом restore_pyc выше. Для сырого .pyc из pyinstxtractor (без полного заголовка) marshal.loads упадёт с ошибкой.Вызов
f.read(16) пропускает 16-байтовый заголовок - парная операция к скрипту восстановления для Python 3.7+. Для образцов Python 3.3–3.6 замените на f.read(12).Деобфускация: статический анализ стилера
Многослойная обфускация (T1027)
Python-стилеры применяют технику Obfuscated Files or Information (T1027) последовательными слоями - матрёшка, где каждый слой прячет следующий. Разбор BlankGrabber от Splunk Threat Research Team показывает типичную архитектуру:Stage 1 - зашифрованный payload внутри PyInstaller: среди извлечённых файлов лежит blob с высокой энтропией (в BlankGrabber - файл
blank.aes). Расшифровка при runtime через AES-GCM с захардкоженным ключом и IV, которые сидят в байткоде первого .pyc-модуля. Нюанс: BlankGrabber использует кастомизированную реализацию AES-GCM, подменяя стандартную pyaes внутри PyInstaller-пакета. Стандартная расшифровка не сработает - нужно воспроизвести модифицированный алгоритм из декомпилированного кода.Stage 2 - обфусцированный Python-скрипт: расшифрованный архив содержит ещё один .pyc (
stub-o.pyc в BlankGrabber). После декомпиляции - zlib-сжатый byte-массив, который при распаковке оказывается Python-кодом, закодированным Base64 + ROT13 + реверс строки. Каждый слой снимается тривиально, но их комбинация замедляет ручной разбор.Stage 3 - итоговый payload: конфигурация стилера с C2-адресами, Telegram-токенами, списком целевых приложений и набором anti-sandbox проверок.
XOR-шифрование строк
Самый распространённый метод сокрытия C2-адресов, bot-токенов и API-ключей. Встречается с однобайтовым и многобайтовым ключом:
Python:
def xor_decrypt(data, key):
if isinstance(key, int): # однобайтовый ключ
return bytes(b ^ key for b in data)
# многобайтовый ключ - XOR с циклическим повторением
return bytes(b ^ key[i % len(key)] for i, b in enumerate(data))
encrypted = b'\x82\x85\x8c\x8c\x8f'
print(xor_decrypt(encrypted, 0xED)) # b'Hello'
Как найти XOR-ключ, если он неочевиден. Частотный анализ: если зашифрованный blob содержит много нулевых байт в оригинале (типично для padding в конфигурационных структурах), самый частый байт в шифротексте и есть ключ. Для многобайтовых ключей - анализ позиционных частот. Метод старый как мир, но на этих стилерах работает безотказно.
В XillenStealer, по данным CYFIRMA, C2-адрес не хранится как единая строка - собирается через конкатенацию фрагментов.
strings тут бесполезен, но декомпиляция решает: в восстановленном коде логика сборки видна целиком.AST-подход к массовой деобфускации
Когда обфускатор заменяет строковые литералы на вызовы функций расшифровки - паттернd('\x48\x65\x6c\x6c\x6f') вместо 'Hello' - руками не масштабируется. Модуль ast стандартной библиотеки Python позволяет написать трансформер: обойти синтаксическое дерево, найти все вызовы дешифратора, выполнить их и подставить результат. Тысячи обфусцированных строк - за секунды.Когда AST не спасает: обфускатор использует
exec() / eval() для динамического исполнения - статический AST-разбор бессилен, код генерируется только при runtime. Тут единственный путь - динамический анализ.Динамический анализ вредоносного ПО: от sandbox до C2-адреса
Подготовка sandbox и обход anti-sandbox
Динамический анализ требует изолированной среды с несколькими настройками, без которых стилер просто не заведётся.Минимальный стек: VM (VirtualBox или VMware Workstation), FakeNet-NG для перехвата DNS/HTTP без реального интернета, Wireshark на хосте или внутри VM, Process Monitor для файловых операций. RAM VM - минимум 8 ГБ. ОС - Windows 10 с «реалистичным» набором софта (браузеры, офис). Голая система без приложений выдаёт sandbox с первого взгляда.
Перед запуском образца стилеры проводят проверки окружения - техника System Checks (T1497.001, тактики: Stealth, Discovery). Что проверяют BlankGrabber и XillenStealer:
Аппаратные маркеры. MAC-адреса с OUI виртуальных сред: 00:0C:29 (VMware), 08:00:27 (VirtualBox). Модель оборудования через WMI-запросы к
Win32_ComputerSystem - строки vmware, virtual, qemu. BlankGrabber, по данным Splunk, содержит расширенный чёрный список UUID, имён компьютеров и аккаунтов, характерных для sandbox-ферм.Драйверы и процессы. Наличие
vboxguest.sys, VBoxService.exe, драйверов VMware. XillenStealer проверяет это в функции check_vm_sandbox() вместе с вызовом IsDebuggerPresent.Сетевая проверка. Соединение со случайно сгенерированным URL: если запрос не возвращает ошибку - сеть считается симулированной. FakeNet-NG по умолчанию отвечает на любой запрос, так что настройте выборочный DNS, чтобы рандомные домены оставались без ответа.
Реестр адаптеров. Запрос имён сетевых адаптеров - VMware и VirtualBox используют характерные названия драйверов.
Обход для аналитика: сменить MAC-адрес VM, имя компьютера и пользователя на реалистичные, снести Guest Additions / VMware Tools, в FakeNet-NG настроить выборочные DNS-ответы. Несложно, но без этого - тишина в логах.
Для контекста: LummaC2 v4.0, по данным Outpost24, применяет тригонометрический анализ траектории мыши для определения, управляет ли системой человек. Этот уровень anti-sandbox характерен для нативных C-стилеров; в Python-based семействах такого пока не встречал.
Перехват C2-коммуникации
При запуске образца в sandbox с FakeNet-NG все сетевые обращения перехватываются. Ключевые паттерны эксфильтрации:Telegram Bot API - самый ходовой канал эксфильтрации в Python-стилерах (BlankGrabber, XillenStealer). Перехватываются POST-запросы к
api.telegram.org/bot<TOKEN>/sendDocument или sendMessage. Из запроса достаём: bot token (в URL после /bot), chat ID (параметр chat_id в теле POST), содержимое эксфильтрации (вложенные файлы). XillenStealer формирует одновременно report.txt и report.html, а архивы свыше 45 МБ нарезает функцией split_large_file(), удаляя каждую часть с диска после успешной отправки.HTTP POST на выделенный сервер - у стилеров с собственной серверной инфраструктурой. В Wireshark: фильтр
http.request.method == POST. Тело запроса - JSON или multipart form-data с собранными данными.IP-lookup сервисы - техника Gather Victim Identity Information (T1589, Reconnaissance). Перед эксфильтрацией стилеры дёргают
ip-api.com или ipinfo.io для получения IP, геолокации и определения типа хостинга. BlankGrabber использует поле hosting в ответе ip-api.com для фильтрации sandbox-хостов в облачных средах. DNS-запросы к этим сервисам - дополнительные IoC.Извлечение C2 из памяти процесса
Если статический анализ не дал C2 - строка собирается динамически, ключ шифрования вычисляется при runtime, или использован нестандартный AES - последний рубеж: дамп памяти.Последовательность: запустить образец в VM, дождаться сетевой активности (в FakeNet-NG появятся DNS-запросы или HTTP-соединения), снять дамп через
procdump -ma <PID> dump.dmp, затем strings -el dump.dmp | grep -iE "http|api.telegram|bot[0-9]".К моменту обращения к C2 все строки расшифрованы в памяти - метод работает даже против многослойного AES с модифицированной реализацией. Одна команда - и токен у вас.
Генерация IOC и правила обнаружения
Индикаторы компрометации
Результат полного цикла анализа - структурированный набор IoC:- Хеши: SHA256 бинаря, извлечённых .pyc-файлов, итогового payload
- Сетевые индикаторы: C2-домены и IP-адреса, Telegram bot token, chat ID, домены IP-lookup сервисов
- Файловые артефакты: имена создаваемых файлов (
report.txt,report.html), пути persistence - Task Scheduler entryWindowsSystemMaintenanceв XillenStealer, Registry Run Keys в BlankGrabber - Поведенческие маркеры: обращения к
Local StateиLogin Dataпрофилей Chromium-браузеров, доступ к директориям крипто-кошельков (exodus.wallet, директории AtomicWallet, Coinomi, Electrum), чтениеtdataTelegram Desktop
Sigma-правила и D3FEND-контрмеры
В SigmaHQ по тегу T1027 доступно 140 правил, включаяproc_creation_win_powershell_xor_encoded_command.yml для обнаружения XOR-кодированных команд. По T1497.001 - правило posh_ps_detect_vm_env.yml фиксирует скрипты, проверяющие виртуальное окружение.Контрмеры MITRE D3FEND для PyInstaller-стилеров:
| D3FEND ID | Контрмера | Применение |
|---|---|---|
| D3-DA | Dynamic Analysis | Sandbox-детонация обфусцированных payload |
| D3-EFA | Emulated File Analysis | Эмуляция исполнения файла для детекции вредоносного поведения, в т.ч. обхода sandbox-проверок |
| D3-FA | File Analysis | Энтропийный анализ PE, проверка overlay-секций |
| D3-SCA | System Call Analysis | Мониторинг syscalls для обнаружения VM-проверок |
Полный workflow: от бинаря до IoC
- Triage: SHA256 в VirusTotal / MalwareBazaar, DiE для энтропии и маркеров PyInstaller
- Распаковка:
pyinstxtractor.py, восстановление .pyc-заголовка, декомпиляция через uncompyle6 или pycdc - Статический анализ: снятие слоёв обфускации, XOR-расшифровка строк, поиск C2-адресов и токенов
- Динамический анализ: sandbox с FakeNet-NG, перехват DNS/HTTP, дамп памяти при необходимости
- Документирование: хеши, сетевые IoC, файловые артефакты, MITRE ATT&CK mapping, Sigma rules
Каждый второй Python-стилер в публичных репозиториях образцов собран из open-source конструктора за вечер. BlankGrabber, XillenStealer, безымянные форки - не APT-инструменты, а конвейер с нулевым порогом входа для атакующего. Именно это и делает их опасными: не сложность отдельного образца, а масштаб. IBM оценивает поток в 6000 наборов credentials в день, и заметная часть - результат «детских» стилеров, которые при этом успешно обходят базовые антивирусные проверки.
Технически ломаются они быстро: XOR-шифрование снимается за минуты, AES с захардкоженным ключом - за час, Telegram-токен из дампа памяти - за одну команду. Но аналитиков, способных пройти цепочку от PyInstaller-бинаря до полного набора IoC, кратно меньше, чем новых образцов каждый день.
Инструментарий доступен и бесплатен: pyinstxtractor, pycdc, FakeNet-NG. Порог входа в анализ вредоносного ПО на Python ниже, чем принято считать. Проблема не в инструментах, а в отсутствии практики на реальных образцах - теория без разбора живого семпла остаётся мёртвым знанием. Собрать навык можно только на задачах - на HackerLab.pro (https://hackerlab.pro) есть reverse-инженерные таски, где цепочка от бинаря до C2 проходится end-to-end.
Последнее редактирование модератором: