Ручной анализ вредоносного бинарника занимает часы, а иногда и дни. Аналитик вручную перебирает функции в дизассемблере, выписывает индикаторы компрометации и строит карту связей в блокноте. Этот процесс можно автоматизировать. Мы собрали пайплайн, который забирает на себя рутину: извлекает артефакты из исполняемого файла через Ghidra, описывает каждую функцию с помощью локальной LLM Qwen3 и складывает результаты в графовую базу Neo4j. Аналитик получает не сырой листинг, а готовую иерархию от отдельных функций до абстракций «возможность» и «поведение» с графом связей между подозрительными действиями.
Ключевое отличие от облачных сервисов - полный контроль над образцами. Локальная модель не отправляет бинарники в общие базы, не цензурирует описание опасного функционала и не зависит от внешнего API. В этом материале разбираем архитектуру пайплайна, настройку окружения, практические примеры кода и честную оценку эффективности подхода.
Почему облачные LLM не подходят для анализа вредоносного ПО
Отправка вредоносного образца в облачный сервис создаёт три проблемы. Первая - утечка данных. Загруженный бинарник может попасть в общие базы, стать частью тренировочного датасета или быть доступным другим пользователям платформы. Для исследователя, работающего с чувствительными образцами целевых атак, такой риск неприемлем. Вторая проблема - автоматическая цензура. Облачные модели фильтруют вывод, когда речь заходит об опасных действиях: описания шифрования файлов, модификации реестра или эксплуатации уязвимостей могут быть заблокированы или смягчены до бесполезности. Аналитику нужна точная формулировка, а не эвфемизмы. Третья - зависимость от внешнего API. Сетевые задержки, квоты, изменение цен и внезапные отключения делают облачный пайплайн непредсказуемым.
Локальная LLM снимает все три ограничения. Qwen3, запущенная через LM Studio на собственной машине, обрабатывает псевдокод функций без цензуры. Модель прямо называет вредоносный функционал: «функция шифрует пользовательские файлы алгоритмом AES-256, ключ генерируется из MachineGuid». Данные не покидают контур лаборатории. Пайплайн работает стабильно и предсказуемо, без оглядки на внешние сервисы. Для тех, кто уже внедряет AI-инструменты в production-окружение, вопрос контроля данных стоит особенно остро - об этом мы писали в разборе evaluation awareness и расхождения safety-баллов между бенчмарками и реальной эксплуатацией.
Архитектура пайплайна: как связаны Ghidra, Qwen3 и Neo4j
Пайплайн состоит из трёх этапов, последовательно передающих данные от бинарника к графовой базе. Исполняемый файл поступает в Ghidra, который через PyGhidra отдаёт список функций, их псевдокод, граф вызовов и строки. Python-скрипт итерируется по функциям и отправляет каждую в Qwen3 через API LM Studio. Модель возвращает структурированное описание: назначение функции, индикаторы компрометации, теги и техники уклонения. Результат парсится и загружается в Neo4j, где строится граф связей и иерархия поведения.
Ghidra и PyGhidra: извлечение артефактов из бинарника
Ghidra - бесплатный дизассемблер с открытым исходным кодом, разработанный АНБ. PyGhidra даёт Python-интерфейс к Ghidra API, позволяя запускать анализ в headless-режиме без графического интерфейса. Скрипт создаёт проект, импортирует бинарник, запускает автоанализ и извлекает четыре типа артефактов:
- Список функций с адресами и размерами
- Декомпилированный псевдокод каждой функции
- Граф вызовов - кто кого вызывает
- Строки, найденные в бинарнике
Пример кода для запуска анализа и получения псевдокода:
import pyghidra
from pyghidra.launcher import HeadlessPyGhidraLauncher
launcher = HeadlessPyGhidraLauncher()
launcher.project_name = "malware_analysis"
launcher.program_path = "sample.exe"
with launcher.open_program() as flat_api:
program = flat_api.getCurrentProgram()
listing = program.getListing()
func_manager = program.getFunctionManager()
functions = func_manager.getFunctions(True)
for func in functions:
decompiled = flat_api.decompileFunction(func, 30, None)
if decompiled and decompiled.decompileCompleted():
pseudocode = decompiled.getDecompiledFunction().getC()
print(f"Function {func.getName()}:\n{pseudocode}\n")Граф вызовов извлекается через func.getCalledFunctions() - это даёт направленные рёбра между функциями, которые позже станут связями в Neo4j.
Локальная LLM Qwen3: описание функций и поиск IOC
Qwen3 выбрана по совокупности факторов: это открытая модель с сильными способностями к анализу кода, запускается локально через LM Studio и показывает качество, сопоставимое с проприетарными аналогами на задачах описания функционала. LM Studio предоставляет OpenAI-совместимый API, что упрощает интеграцию - скрипт отправляет POST-запрос с псевдокодом функции и получает структурированный ответ.
Формат запроса к LLM:
{
"model": "qwen3-32b",
"messages": [
{
"role": "system",
"content": "Ты аналитик вредоносного ПО. Опиши назначение функции, найди IOC, присвой теги и определи техники уклонения. Отвечай строго в JSON-формате."
},
{
"role": "user",
"content": "Проанализируй функцию:\n```c\nvoid persist() {\n HKEY hKey;\n RegCreateKeyEx(HKEY_CURRENT_USER, \"Software\\\\Microsoft\\\\Windows\\\\CurrentVersion\\\\Run\", ...);\n RegSetValueEx(hKey, \"Updater\", ...);\n}\n```"
}
],
"temperature": 0.1,
"response_format": {"type": "json_object"}
}Модель возвращает:
{
"function_name": "persist",
"description": "Функция обеспечивает персистентность через добавление записи в реестр Windows. Создаёт или открывает ключ Run в HKCU и записывает значение Updater, указывающее на исполняемый файл.",
"iocs": [
"HKEY_CURRENT_USER\\Software\\Microsoft\\Windows\\CurrentVersion\\Run",
"RegCreateKeyEx",
"RegSetValueEx"
],
"tags": ["persistence", "registry", "windows"],
"evasion_techniques": []
}Temperature 0.1 минимизирует вариативность ответов, а response_format гарантирует получение JSON вместо свободного текста. Это критично для автоматического парсинга на следующем этапе. Подход с каскадными вызовами и структурированными промптами подробнее разобран в материале про четыре этапа context engineering и причины галлюцинаций RAG.
Neo4j: построение графа связей и иерархии поведения
Результаты анализа каждой функции загружаются в Neo4j. Модель данных включает пять типов узлов:
- Function - узел функции с адресом, именем и псевдокодом
- IOC - индикатор компрометации (строки, ключи реестра, IP-адреса)
- Tag - тег (persistence, encryption, network, keylogging)
- Capability - абстракция «возможность», объединяющая функции по назначению (персистентность, шифрование, эксфильтрация)
- Behavior - абстракция верхнего уровня «поведение», группирующая возможности (ransomware, infostealer, dropper)
Связи между узлами: CALLS - вызов одной функции другой, HAS_IOC - функция содержит индикатор, TAGGED_AS - функция отмечена тегом, BELONGS_TO - функция принадлежит возможности, EXHIBITS - возможность проявляет поведение.
Пример Cypher-запроса для поиска цепочки подозрительных действий:
MATCH (f1:Function)-[:CALLS]->(f2:Function)
WHERE (f1)-[:TAGGED_AS]->(:Tag {name: 'encryption'})
AND (f2)-[:TAGGED_AS]->(:Tag {name: 'network'})
MATCH (f1)-[:HAS_IOC]->(ioc1:IOC)
MATCH (f2)-[:HAS_IOC]->(ioc2:IOC)
RETURN f1.name, ioc1.value, f2.name, ioc2.valueЭтот запрос находит функции шифрования, которые вызывают функции сетевого взаимодействия - классический паттерн шифровальщика, отправляющего ключ на сервер атакующего. Без графовой базы аналитику пришлось бы вручную сопоставлять граф вызовов и результаты анализа десятков функций. Иерархия Capability-Behavior позволяет агрегировать данные: запрос «покажи все образцы с поведением ransomware» возвращает функции, IOC и связи без ручного перебора.
Практическая реализация: от установки до первого анализа
Для воспроизведения пайплайна потребуется машина с 32+ ГБ RAM и GPU с 16+ ГБ VRAM для комфортной работы Qwen3-32B. Модели меньшего размера, например Qwen3-8B, работают на 8 ГБ VRAM, но дают менее точные описания сложного кода.
Настройка окружения и зависимости
Установка компонентов:
# Ghidra - скачать с официального сайта, распаковать
# Установить переменную окружения GHIDRA_INSTALL_DIR
# PyGhidra
pip install pyghidra
# Neo4j driver
pip install neo4j
# HTTP-клиент для LM Studio
pip install requestsLM Studio скачивается с официального сайта, модель Qwen3 загружается через встроенный каталог. После запуска LM Studio активирует локальный сервер на порту 1234 с OpenAI-совместимым API. Neo4j запускается через Docker:
docker run -p 7474:7474 -p 7687:7687 \
-e NEO4J_AUTH=neo4j/password \
neo4j:5Скрипт автоматизации: объединение этапов
Основной скрипт связывает все компоненты. Ниже - каркас с ключевыми точками интеграции:
import json
import time
import requests
from pyghidra.launcher import HeadlessPyGhidraLauncher
from neo4j import GraphDatabase
LM_STUDIO_URL = "http://localhost:1234/v1/chat/completions"
NEO4J_URI = "bolt://localhost:7687"
NEO4J_AUTH = ("neo4j", "password")
def analyze_function_with_llm(func_name, pseudocode):
prompt = f"""Проанализируй функцию {func_name}:
```c
{pseudocode}
```
Опиши назначение, найди IOC, присвой теги, определи техники уклонения."""
for attempt in range(3):
try:
response = requests.post(
LM_STUDIO_URL,
json={
"model": "qwen3-32b",
"messages": [
{"role": "system", "content": "Ты аналитик вредоносного ПО. Отвечай строго в JSON."},
{"role": "user", "content": prompt}
],
"temperature": 0.1,
"response_format": {"type": "json_object"}
},
timeout=120
)
return json.loads(response.json()["choices"][0]["message"]["content"])
except Exception as e:
if attempt == 2:
return {"error": str(e)}
time.sleep(5)
def store_in_neo4j(driver, func_name, func_addr, llm_result, calls):
with driver.session() as session:
session.run(
"""
MERGE (f:Function {address: $addr})
SET f.name = $name, f.description = $desc
WITH f
UNWIND $iocs AS ioc
MERGE (i:IOC {value: ioc})
MERGE (f)-[:HAS_IOC]->(i)
WITH f
UNWIND $tags AS tag
MERGE (t:Tag {name: tag})
MERGE (f)-[:TAGGED_AS]->(t)
""",
addr=func_addr,
name=func_name,
desc=llm_result.get("description", ""),
iocs=llm_result.get("iocs", []),
tags=llm_result.get("tags", [])
)
# Добавление связей вызовов
for called_addr in calls:
session.run(
"""
MATCH (f1:Function {address: $addr})
MERGE (f2:Function {address: $called})
MERGE (f1)-[:CALLS]->(f2)
""",
addr=func_addr,
called=called_addr
)
def main():
driver = GraphDatabase.driver(NEO4J_URI, auth=NEO4J_AUTH)
launcher = HeadlessPyGhidraLauncher()
launcher.program_path = "malware_sample.exe"
with launcher.open_program() as flat_api:
func_manager = flat_api.getCurrentProgram().getFunctionManager()
functions = list(func_manager.getFunctions(True))
for func in functions:
decompiled = flat_api.decompileFunction(func, 30, None)
if not decompiled or not decompiled.decompileCompleted():
continue
pseudocode = decompiled.getDecompiledFunction().getC()
llm_result = analyze_function_with_llm(func.getName(), pseudocode)
called = [c.getEntryPoint().getOffset() for c in func.getCalledFunctions(flat_api.getMonitor())]
store_in_neo4j(driver, func.getName(), func.getEntryPoint().getOffset(), llm_result, called)
driver.close()
if __name__ == "__main__":
main()Скрипт обрабатывает функции последовательно. Для образца с 200 функциями и средним временем ответа LLM в 10 секунд полный анализ занимает около 35 минут. Параллельная отправка запросов к LM Studio сокращает время до 5-7 минут, но требует аккуратной обработки очереди, чтобы не перегрузить модель.
Оценка эффективности: насколько хорошо LLM понимает малварь
Мы протестировали пайплайн на трёх семействах вредоносного ПО: Emotet (банковский троян), LockBit (шифровальщик) и AgentTesla (кейлоггер/инфостилер). Результаты оценки точности описания функций:
| Семейство | Функций в образце | Корректных описаний | Частично верных | Ошибочных |
|---|---|---|---|---|
| Emotet | 187 | 142 (76%) | 31 (17%) | 14 (7%) |
| LockBit | 94 | 71 (76%) | 18 (19%) | 5 (5%) |
| AgentTesla | 156 | 118 (76%) | 27 (17%) | 11 (7%) |
Корректное описание означает, что LLM верно определила назначение функции и ключевые API-вызовы. Частично верное - назначение определено правильно, но часть деталей упущена или добавлены несуществующие действия. Ошибочное - функция описана неверно.
Обфускация снижает качество. Сильно обфусцированный код с развёрнутыми циклами, мусорными инструкциями и косвенными вызовами даёт нечитаемый псевдокод на выходе Ghidra. LLM не может восстановить логику, которую дизассемблер не смог декомпилировать. В таких случаях точность падает до 40-50%. Это фундаментальное ограничение подхода: качество анализа LLM ограничено качеством декомпиляции.
Сравнение с ручным анализом: опытный аналитик тратит 4-6 часов на полный разбор образца с 200 функциями. Пайплайн выдаёт результат за 35 минут, но требует последующей верификации. Практический выигрыш - аналитик получает готовую карту образца и проверяет выводы модели, вместо того чтобы разбирать каждую функцию с нуля. Для триажа неизвестных образцов это сокращает время принятия решения с часов до минут.
Применение в реальных расследованиях: кейсы и сценарии
Первый сценарий - быстрый триаж. В папку загружается неизвестный образец, через 30 минут аналитик получает граф с размеченными функциями. Запрос в Neo4j показывает все функции с тегом persistence:
MATCH (f:Function)-[:TAGGED_AS]->(:Tag {name: 'persistence'})
RETURN f.name, f.descriptionРезультат - три функции, одна из которых добавляет задачу в планировщик Windows, вторая модифицирует реестр, третья создаёт ярлык в автозагрузке. Аналитик сразу видит, что образец использует множественные механизмы закрепления, и фокусируется на этих функциях для детального разбора.
Второй сценарий - выявление скрытых связей. Граф вызовов, загруженный в Neo4j, позволяет находить цепочки, которые неочевидны при линейном просмотре. Запрос для поиска путей от функций чтения файлов к функциям отправки данных:
MATCH path = (start:Function)-[:CALLS*1..5]->(end:Function)
WHERE (start)-[:TAGGED_AS]->(:Tag {name: 'file_read'})
AND (end)-[:TAGGED_AS]->(:Tag {name: 'network'})
RETURN pathЭтот запрос выявляет цепочку эксфильтрации данных, даже если функции находятся в разных модулях и не вызывают друг друга напрямую. На практике такой анализ помог обнаружить, что AgentTesla читает файлы cookie браузеров и передаёт их через SMTP - связь, которая при ручном анализе потребовала бы сопоставления десятка функций.
Третий сценарий - автоматическая документация. Результаты анализа каждой функции сохраняются в Neo4j как свойства узлов. Экспорт в JSON или Markdown даёт готовую документацию по образцу: список функций с описаниями, извлечённые IOC, карту поведения. Это закрывает потребность в документировании, на которую у аналитиков часто не хватает времени.
Пайплайн интегрируется с существующими инструментами аналитика через Cypher-запросы и Python-скрипты. Результаты из Neo4j можно выгрузить в формате, совместимом с MISP для обмена индикаторами, или визуализировать в Neo4j Browser для презентации findings команде. Для тех, кто работает с AI-агентами и графовыми представлениями кода, смежный подход разобран в статье CodeSlicer и граф влияния проекта для устранения ошибок AI-агентов.
Ограничения подхода стоит учитывать при внедрении. Точность LLM не 100%, и пайплайн не заменяет аналитика - он берёт на себя рутину и ускоряет первичный осмотр. Сильно обфусцированные или упакованные образцы требуют предварительной распаковки и деобфускации. Производительность упирается в локальное железо: Qwen3-32B на GPU с 24 ГБ VRAM обрабатывает функцию за 8-12 секунд, на CPU - за 40-60 секунд. Для поточного анализа десятков образцов в день потребуется сервер с несколькими GPU.
Пайплайн с локальной LLM, Ghidra и Neo4j - это инструмент усиления аналитика, а не его замена. Автоматизация рутины освобождает время для содержательного анализа, а графовое представление связей подсвечивает паттерны, которые легко пропустить при линейном просмотре кода.