Дайджесты новостей
Концептуальная иллюстрация реверс-инжиниринга бинарного кода: трансформация ассемблерных опкодов в чистый код на C и Rust через аналитическую призму Ghidra и нейросетей.

Декомпиляция и реверс-инжиниринг бинарного кода с помощью LLM: от Ghidra к чистому C и Rust

Каждый инженер, хоть раз открывавший дизассемблер, помнит это гнетущее чувство интеллектуального тупика. Перед глазами бесконечные листинги инструкций mov, lea и вызовы функций вроде func_004018A0. Инструменты декомпиляции переводят опкоды в Си-подобный псевдокод, но мешанина из нетипизированных указателей void*, переменных uVar1 и раздутых условий далека от живого исходного кода.

Десятилетиями реверс-инжиниринг оставался медитативным, филигранным и дорогим ремеслом. На восстановление исходников небольшой утилиты или консольной игры уходили месяцы ручного труда. Однако появление специализированных моделей (вроде LLM4Decompile) и рассуждающих агентов меняет эту сферу. Объединение анализатора NSA Ghidra с языковыми моделями сформировало замкнутый контур, автоматически превращающий бинарные дампы в чистый, типобезопасный код на C или Rust.

Туман декомпиляции: почему классический статический анализ застревает на полпути

Чтобы оценить масштаб прорыва, важно понять, что компилятор с жесткими оптимизациями (-O2, -O3) безжалостно стирает метаинформацию:

  1. Стирание типов и структур: объекты превращаются в плоские смещения относительно регистров (например, *(int*)(ESI + 0x18)). Декомпилятор видит доступ к байтам памяти, но не знает, что это: поле структуры профиля или ширина графического буфера.
  2. Инлайнинг функций и разворачивание циклов: лаконичные вызовы растворяются внутри вызывающего кода, превращаясь в длинные монолитные последовательности инструкций.
  3. Размытие управляющих конструкций: операторы switch и if-else превращаются в таблицы переходов и хаос безусловных прыжков jmp.

Классический декомпилятор восстанавливает лишь поток управления и приблизительную арифметику. Смысловую логику до последнего времени мог извлечь только человек.

Двухлетний марафон за 84 дня: практический прорыв на реальных бинарниках

Показательный пример произошел в сообществе ретро-инженерии. Специалист по реверсу Крис Льюис потратил почти два года кропотливой работы на декомпиляцию бинарного кода игры Snowboard Kids для приставки Nintendo 64 — тысячи часов за ассемблерными листингами и ручным сопоставлением байтов.

Когда тот же процесс воспроизвели с использованием агентных LLM-циклов, срок реконструкции сократился до 84 дней — почти в девять раз быстрее. По оценкам экспертов по безопасности, с развитием моделей время восстановления типовых бинарных модулей сжимается до считанных дней.

Модель в этой связке работает как эксперт по распознаванию алгоритмических паттернов: видя разрозненные битовые сдвиги и операции XOR, она узнает реализацию шифрования AES или расчет контрольной суммы CRC32 и заменяет сотню строк псевдокода на одну чистую функцию.

Архитектура верифицируемого контура: компилятор как беспристрастный судья

Главная опасность слепого доверия нейросетям в системном программировании — скрытые галлюцинации. Решением стал подход Verifiable Feedback Loop (Верифицируемый цикл с обратной связью), где модель работает в постоянном спарринге с компилятором:

  • Шаг 1: Декомпилятор Ghidra извлекает сырой псевдокод из целевого бинарника;
  • Шаг 2: LLM анализирует псевдокод, восстанавливает типы данных, дает осмысленные имена переменным и генерирует код на Rust;
  • Шаг 3: Компилятор (rustc) пытается собрать код. При ошибках синтаксиса или заимствования ссылок диагностический лог возвращается в контекст модели;
  • Шаг 4: Тестовый стенд запускает оригинальный и скомпилированный бинарники на одинаковых входных данных, сверяя дампы памяти и поток STDOUT;
  • Шаг 5: Цикл повторяется до полной функциональной идентичности.

Инфографика верифицируемого цикла реверс-инжиниринга: замкнутый контур декомпиляции в Ghidra, генерации кода LLM, проверки компилятором rustc и тестирования вывода.

Извлечение псевдокода без интерфейса: автоматизация Ghidra в режиме Headless

Для построения конвейера ручные клики в интерфейсе исключаются. Используется консольная утилита analyzeHeadless и Java-скрипт декомпиляции (DecompileExport.java):

import ghidra.app.script.GhidraScript;
import ghidra.app.decompiler.*;
import ghidra.program.model.listing.*;
import java.io.PrintWriter;

public class DecompileExport extends GhidraScript {
    @Override
    public void run() throws Exception {
        DecompInterface decompiler = new DecompInterface();
        decompiler.openProgram(currentProgram);
        PrintWriter writer = new PrintWriter(getScriptArgs()[0]);

        FunctionManager funcManager = currentProgram.getFunctionManager();
        for (Function func : funcManager.getFunctions(true)) {
            DecompileResults results = decompiler.decompileFunction(func, 60, monitor);
            if (results.decompileCompleted()) {
                writer.println("// Function: " + func.getName());
                writer.println(results.getDecompiledFunction().getC());
            }
        }
        writer.close();
    }
}

Запуск декомпилятора выполняется из консоли без подъема графического сервера:

# Извлечение псевдокода из исполняемого файла target_binary
/opt/ghidra/support/analyzeHeadless /tmp/ghidra_workspace ProjectTemp \
    -import ./target_binary \
    -postScript DecompileExport.java ./raw_decompiled.c \
    -deleteProject

Трансформация псевдокода в безопасный Rust: связка Python и компилятора

Связующий скрипт на Python передает фрагмент псевдокода в языковую модель, запрашивает переписывание на Rust и немедленно проверяет результат через rustc:

import os
import subprocess
from openai import OpenAI

client = OpenAI(api_key=os.environ.get("OPENAI_API_KEY"))

def reconstruct_to_rust(raw_c_code: str) -> str:
    system_prompt = (
        "Ты — эксперт по системному реверс-инжинирингу. Преобразуй псевдокод Ghidra "
        "в идиоматичный, типобезопасный Rust. Восстанови осмысленные имена и структуры данных. "
        "Верни только чистый блок кода без пояснений."
    )
    response = client.chat.completions.create(
        model="gpt-4o",
        messages=[
            {"role": "system", "content": system_prompt},
            {"role": "user", "content": f"Псевдокод:\n```c\n{raw_c_code}\n```"}
        ],
        temperature=0.1
    )
    text = response.choices[0].message.content.strip()
    return text.removeprefix("```rust").removeprefix("```").removesuffix("```").strip()

def verify_compilation(rust_code: str, output_path: str = "./reconstructed.rs") -> bool:
    with open(output_path, "w", encoding="utf-8") as f:
        f.write(rust_code)
    proc = subprocess.run(["rustc", "--crate-type=lib", output_path], capture_output=True, text=True)
    if proc.returncode == 0:
        print("[УСПЕХ] Rust-код валиден и скомпилирован.")
        return True
    print(f"[ОШИБКА]: {proc.stderr}")
    return False

if __name__ == "__main__":
    with open("./raw_decompiled.c", "r", encoding="utf-8") as f:
        pseudocode = f.read()
    rust_code = reconstruct_to_rust(pseudocode)
    verify_compilation(rust_code)

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

Методология чистой комнаты: как легально воссоздавать закрытый софт

Понимание логики бинарников открывает дорогу легальному воссозданию проприетарных систем по принципу «чистой комнаты» (clean-room design). Закон защищает конкретную форму выражения кода, но не функциональный протокол. Методология разделяет две группы:

  1. Группа анализа (Dirty Room): исследует закрытый бинарник, декомпилирует функции и составляет спецификацию протоколов и структур;
  2. Группа разработки (Clean Room): не видит закрытого кода и пишет независимую реализацию с нуля по составленному описанию.

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

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

При всех успехах метод имеет инженерные ограничения:

  • Агрессивный инлайнинг и векторизация: код с флагами SIMD/AVX дробится на параллельные регистровые операции, восстановить смысл которых сложно даже передовым LLM;
  • Протекторы и обфускаторы: коммерческие упаковщики (VMProtect, Denuvo) требуют предварительного ручного снятия виртуализации;
  • Изоляция сред: декомпиляция недоверенных файлов должна проходить в одноразовых песочницах без доступа к сети.

Связка Ghidra и кодовых моделей лишает закрытое ПО статуса «вечного черного ящика»: старые библиотеки получают вторую жизнь в безопасном Rust, а команды безопасности — инструмент мгновенного разбора инцидентов.