Проведение многих последних исследований вредоносного программного обеспечения автоматизируется с использование больших языковых моделей (БЯМ, Large Language Model, LLM). Исследователь загружает декомпилированный псевдокод отдельного модуля вредоноса из IDA Pro / Ghidra и отправляет в БЯМ с просьбой разъяснить код.

У данного подхода есть ряд недостатков:

  • часть IOC (Indicator of Compromise) сливается в общую базу выбранной модели;

  • некоторые открытые модели могут цензурить описание некоторых модулей, из‑за чего исследователь пропустит описание функционала;

  • структура вредоносной программы может быть довольно запутанной, что приведет к переполнению контекста диалога БЯМ (привет Claude, с покупкой токенов за каждые 3 вопроса).

При количестве функций в районе 100–200 (типичный небольшой троян или шифровальщик) это часы рутинной работы.

В связи с развитием локальных LLM возникла идея автоматизировать первоначальный проход по псевдокоду вредоноса для автоматического выявления части функционала и подсвечивания наиболее значимых для исследования функций.

Для этого предлагается соединить следующие технологии:

  1. PyGhidra (Python‑биндинг Ghidra) — для дизассемблирование и декомпиляция без графического интерфейса для удобства парсинга данных и загрузки в базу.

  2. Neo4j — база данных, для удобного представления функций из дизассмблера в виде графа. Выбрана из‑за удобства последующего обхода последовательностей вызовов функций и последующего добавления описания из БЯМ.

  3. Локальная LLM (Qwen3, запущенная через LM Studio с API‑ключом для доступа) — для анализа каждой функции.

Архитектура системы

Графически структура проекта может быть представлена следующим образом:

Структура LLM-системы анализа вредоносных программ
Структура LLM‑системы анализа вредоносных программ

По данной структуре навайбкодился за пару вечеров проект, исходный код которого можно увидеть в репозитории: Dvoranchik/MLMalwareAnalyzer: Analyze Malwares by local LLM.

Процесс обработки вредоносной программы описывается следующими последовательными шагами, реализованными в модуле pipeline.py:

  1. Извлечение фактов (ghidra_extractor.py) — PyGhidra дизассемблирует и декомпилирует бинарник.
    Для каждой функции извлекается:
    — адрес, имя, размер, цикломатическая сложность (число базовых блоков);
    — декомпилированный C‑псевдокод через DecompInterface (с таймаутом 30 секунд на функцию и обрезкой до 6000 символов, чтобы не ломать контекст LLM);
    — граф вызовов (getCalledFunctions);
    — строковые ссылки (getReferencesFrom + hasStringValue);
    — таблица импортов (ExternalManager).
    Все это загружается в Neo4j.
    Отдельно вычисляются метаданные образца (формат PE, архитектура/LanguageID из Ghidra, точки входа) и все стандартные хэши файла (MD5, SHA1, SHA256).

  2. LLM‑анализ функций (worker.py + mcp.py) — каждая функция по очереди отправляется в локальную LLM с запросом на выполнение анализа: назначение, IOC, теги, сетевые индикаторы, команды запуска, техники уклонения. Все это возвращается в виде JSON.
    Каждой функции в промпт передаётся не только декомпилированный код, но и её позиция в графе вызовов — какие функции она вызывает и кто вызывает её. Это даёт модели минимальный «контекст соседей».

  3. Агрегация возможностей (aggregator.py) — теги и категории со всех проанализированных функций собираются в узлы Capability с весами.

  4. Построение цепочек поведения (builder.py) — на основе комбинаций Capability и графа вызовов выполняется поиск поведенческих паттернов (C2-коммуникация, шифрование файлов, антидетект и так далее).

  5. Генерация отчёта (generator.py) — основные накопленные данные снова выгружаются моделью из Neo4j, анализируются и собираются в отчет.

Модели данных в Neo4j

Если анализировать бинарник «в лоб», результат ложится в одну плоскую таблицу: строка на функцию, в ней колонки “summary”, “tags”, “importance” и так далее.
Проблема в том, что реальный вопрос, который интересует аналитика, почти никогда не звучит как «покажи мне строки, где tag = crypto». Ему необходимо: «функция шифрует файлы — вызывает ли она при этом что‑то из тех функций, что открывают сетевое соединение?», «эти пять функций помечены как anti‑debug — связаны ли они между собой вызовами, или это никак не взаимодействующие проверки?».

Граф решает это следующим образом: функция — это узел, вызов другой функции — это ребро между узлами. Вопрос «связаны ли эти пять функций вызовами» превращается в один Cypher‑запрос по графу, а не в рекурсивный JOIN. По этой причине в проекте есть отдельные уровни абстракции — не только «Функция» и «Анализ этой функции», но и «Возможность» (Capability) и «Поведение» (Behavior).

Получаем четыре смысловых уровня:

  1. Sample — хэши, формат, архитектура.

  2. Function — конкретная функция из файла: адрес, декомпилированный код, размер.

  3. Analysis — результат разбора функции локальной LLM: что она делает, какие у неё IOC, какие теги.

  4. Capability — Behavior — обобщения на основе множества функций: сначала отдельный функционал бинарника (умеет работать с файлами, умеет шифровать), а затем — уже интерпретация их в поведение («если есть работа с файлами И шифрование одновременно — скорее всего, шифровальщик»).

Каждый следующий уровень строится от предыдущего отдельным шагом, и на каждом уровне — в граф вносятся новые узлы.

Узлы имеют следующие типы:

  • Sample — один узел на весь анализируемый файл. Хранит все виды хэшей (MD5, SHA1, SHA256), формат (PE/ELF), архитектуру и точки входа.

  • Function — один узел на каждую функцию, хранит: адрес как уникальный идентификатор, имя (из Ghidra FUN_xxxxx), декомпилированный C‑псевдокод, метрика сложности (число базовых блоков), флаг «это библиотечная функция без своего тела» и флаг «уже проанализирована LLM».

  • String — строковые константы, на которые ссылаются функции. Необходимо, чтобы увидеть все функции, обращающиеся к одной и той же подозрительной строке.

  • Import — запись из таблицы импортов PE‑файла: какая DLL и какая функция из неё используется.

  • Analysis — результат разбора одной функции локальной LLM.

  • Capability — обобщённый функционал бинарника, собранный агрегацией тегов сразу с нескольких функций (например, «умеет работать с файлами» — это может быть подтверждено 40 разными функциями). Хранит вес (strength) — насколько часто эта способность подтверждается по всему бинарнику.

  • Behavior — итоговая интерпретация: комбинация нескольких Capability, сложенная в конкретное поведение верхнего уровня («шифрование файлов», «связь с C2-сервером»).

Связи узлов могут быть выражены следующим образом:

Связи узлов в базе данных Neo4j
Связи узлов в базе данных Neo4j

Проверка работоспособности

Проверка программы проводилась на образцах WannaCry из открытых источников (MalwareBazaar). После полного прогона (~195 функций, анализ занял 126 минут, при моих мощностях в 60 ГБ оперативной памяти и Nvidia 5070 TI) отчёт показал три поведенческих паттерна с уверенностью 100%: File Encryption (40 функций‑доказательств), C2 Communication, Anti‑Analysis. YARA‑правило DebuggerCheck__API на сайте подтвердило найденный пайплайном anti-debug паттерн.

В будущем думаю еще доработать архитектуру вызовов и улучшить промтинг для поиска дополнительных индикаторов.