Перейти к содержанию
Новое AiManual теперь в MAX Подписаться
Публикация AiManual

Автоматизация реверс-инжиниринга вредоносного ПО: локальная LLM, Ghidra и Neo4j в одном пайплайне

Практическое руководство по автоматизации реверс-инжиниринга вредоносного ПО: собираем пайплайн из Ghidra, локальной LLM Qwen3 и Neo4j для быстрого анализа функ

Коротко

Что будет в материале

  1. 01

    Почему облачные LLM не подходят для анализа вредоносного ПО

  2. 02

    Архитектура пайплайна: как связаны Ghidra, Qwen3 и Neo4j

  3. 03

    Практическая реализация: от установки до первого анализа

  4. 04

    Оценка эффективности: насколько хорошо LLM понимает малварь

Ручной анализ вредоносного бинарника занимает часы, а иногда и дни. Аналитик вручную перебирает функции в дизассемблере, выписывает индикаторы компрометации и строит карту связей в блокноте. Этот процесс можно автоматизировать. Мы собрали пайплайн, который забирает на себя рутину: извлекает артефакты из исполняемого файла через 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 requests

LM 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 (кейлоггер/инфостилер). Результаты оценки точности описания функций:

СемействоФункций в образцеКорректных описанийЧастично верныхОшибочных
Emotet187142 (76%)31 (17%)14 (7%)
LockBit9471 (76%)18 (19%)5 (5%)
AgentTesla156118 (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 - это инструмент усиления аналитика, а не его замена. Автоматизация рутины освобождает время для содержательного анализа, а графовое представление связей подсвечивает паттерны, которые легко пропустить при линейном просмотре кода.

Подписаться на канал