Nyxstone — (Диз)ассемблерный фреймворк на основе LLVM

D2

Администратор
Регистрация
19 Фев 2025
Сообщения
4,380
Реакции
0
Emproof Nyx - продукт для усиления безопасности встроенных систем через инновационные техники перезаписи двоичных файлов. Он защищает софт от реверса и эксплойтов с помощью статической инструментации бинарей, добавляя такие меры защиты как обфускация кода, анти-дебаг и анти-тампер чеки, а также митигации эксплойтов типа стековых канареек и контроля потока выполнения.
Перезапись бинарников не зависит от компилятора и языка программирования, что даёт гибкость в модификации и хардении кода. Процесс включает три шага:
  1. Преобразование бинаря в промежуточное представление, которое не зависит от архитектуры.
  2. Трансформация этого представления
  3. Понижение обратно в машинный код и запись обратно в двоичный.
Для первого и третьего шагов Nyx использует функционал ассемблирования и дизассемблирования инструкций.
Изначально использовались Capstone для дизассемблирования и Keystone для ассемблирования, но эти фреймворки оказались не идеальны. Keystone часто выдавал некорректный машинный код и плохо сообщал об ошибках. Также в нём не имелось поддержки меток в ассемблере.
В результате был создан Nyxstone - библиотека для сборки и ревёрса на базе LLVM. После года тестирования на разных архитектурах, он стал открытым под MIT лицензией.
Nyxstone можно использовать как отдельный инструмент или как библиотеку для C++, Rust и Python.

Ключевые фичи Nyxstone:​

  1. Основан на LLVM из-за мощных возможностей ассемблера и дизассемблера, использует бэкенд LLVM для генерации кода.
  2. Прямая линковка с LLVM обеспечивает компактность и легкость поддержки.
  3. Поддержка всех архитектур, совместимых с LLVM.
  4. Унифицированные возможности ассемблирования/дизассемблирования для простого перевода между машинным и ассемблерным кодом.
  5. Поддержка меток в ассемблере, включая определение произвольных меток и меток для инструкций. Полезно для ассемблирования условных и безусловных переходов, а также инструкций относительно счетчика программы.
  6. Гибкая и детальная конфигурация процессоров и их характеристик, позволяющая настраивать инструмент под конкретные расширения ISA и аппаратные характеристики.
Далее будут подробнее рассмотрены комбинированная функция ассемблирования/дизассемблирования, поддержка меток и опции конфигурации.

Ключевые фичи объединенных возможностей (диз)ассемблирования Nyxstone:​

  1. Интегрированные функции ассемблирования и дизассемблирования в едином фреймворке.
  2. Использование внутреннего ассемблера и дизассемблера LLVM для поддержки различных архитектур.
  3. Для дизассемблирования применяется API LLVM, конвертирующий машинный код в человеко-читаемый ассемблер.
  4. Функции ассемблирования доступны через внутренний бэкенд машинного кода LLVM.
  5. Nyxstone оборачивает внутренние объекты LLVM и хукает релевантные API, что позволяет:
    • Бесшовно использовать функционал ассемблирования LLVM
    • Добавлять дополнительные проверки ошибок без патчинга LLVM
  6. Использование диагностики LLVM для эффективного репорта проблем, включая точное место в ассемблерном коде, где возникла ошибка.
  7. Любая диагностика от LLVM трактуется как ошибка, что обеспечивает точный и полный отчёт ошибок для упрощения дебага и доработки кода.

Поддержка меток​

Важная фича Nyxstone - поддержка меток в ассемблере. В отличие от других фреймворков (например, Keystone), можно определять метки, ссылающиеся на конкретные адреса. Это полезно при ассемблировании условных переходов и инструкций относительно счетчика программы.

Еще один юзкейс - более гибкое размещение последовательностей инструкций. Пример - реализация теневого стека (для защиты адресов возврата от атак):
Код: Скопировать в буфер обмена
Код:
; addr 0x1000
    add r10, 4             ; r10 holds ptr to the shadow stack
    lea rbx, [rip + .ret]  ; load the return address
    mov [r10], rbx         ; store the return address on the shadow stack
    call some_fn           ; call into the function
.ret:
    ; ...

; some functions in-between

; addr 0x1400
some_fn:
    ; ...
    ; addr 0x1458
    mov rbx, [rsp]               ; load return address
    cmp [r10], rbx               ; ensure return address is unchanged
    jne .return_addr_compromised ; at 0x1800
    sub r10, 4                   ; clean up shadow stack
    ret

Nyxstone может ассемблировать такие снипеты отдельно, если указать адреса отсутствующих меток. Для патча на 0x1000 нужен адрес some_fn, а для ассемблирования на 0x1458 - адрес .return_addr_compromised.

Для этого юзкейса важно, чтобы сгенеренный машинный код корректно переходил по пользовательским меткам и ссылался на них. Иначе вставленные в бинарник инструкции могут уйти не туда, что приведет к неправильному потоку выполнения или инвалидным обращениям к памяти.

При разработке Nyxstone обнаружилось, что в некоторых случаях LLVM генерит код, который ссылается на адрес 0 вместо адреса, указанного в метке.

Чтобы Nyxstone не выдавал неверные байты инструкций, были добавлены кастомные перемещения и обработка неучтенных инструкций.

Опции конфигурации​

Использование LLVM в качестве основы для Nyxstone даёт преимущество прямого доступа к опциям конфигурации архитектуры LLVM. От этого можно настраивать Nyxstone под конкретные процессоры и включать всякие расширения архитектуры через фишки процессора.

Например, на ARMv8 инструкции с плавающей точкой являются расширением базового ISA и могут не поддерживаться всеми ARMv8 процами. Настройка процессора в Nyxstone включает его дефолтные расширения, позволяя ассемблировать и дизассемблировать любые инструкции расширений, такие как инструкции с плавающей точкой на ARMv8.

Примеры использования​

Рассмотрим, как применять основные функции Nyxstone на практике. Сначала продемонстрируем их в клиентском инструменте (CLI), а затем посмотрим, как интегрировать Nyxstone в качестве библиотеки в разных ЯП.

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

Указав нужную операцию с помощью флагов -A/--assemble и -D/--disassemble, можно легко переводить инструкции между машинным кодом и языком ассемблера:
Код: Скопировать в буфер обмена
Код:
$ ./nyxstone -A "xor rax, rbx"
#   0x00000000: xor rax, rbx - [ 48 31 d8 ]

$ ./nyxstone -D "13 37"
#   0x00000000: adc esi, dword ptr [rdi] - [ 13 37 ]

$./nyxstone -A "xor rax, rbx; add rax, 10"
#   0x00000000: xor rax, rbx - [ 48 31 d8 ]
#   0x00000003: add rax, 10 - [ 48 83 c0 0a ]

Адреса и метки​

CLI Nyxstone позволяет задавать стартовый адрес для ассемблирования и определять метки. Используя предыдущий пример, можно ассемблировать два фрагмента кода отдельно:
Код: Скопировать в буфер обмена
Код:
$ ./nyxstone --address "0x1000" -A
    "
    add r10, 4
    lea rbx, [rip + .ret]
    mov [r10], rbx
    call some_fn
    .ret:
    " --labels "some_fn=0x1400"
#   0x00001000: add r10, 4 - [ 49 83 c2 04 ]
#   0x00001004: lea rbx, [rip + .ret] - [ 48 8d 1d 08 00 00 00 ]
#   0x0000100b: mov qword ptr [r10], rbx - [ 49 89 1a ]
#   0x0000100e: call some_fn - [ e8 ed 03 00 00 ]

$ ./nyxstone --address "0x1458" -A
    "
    mov rbx, [rsp + 4]
    cmp rbx, [r10]
    jne .return_addr_compromised
    sub r10, 4
    ret
    " --labels ".return_addr_compromised=0x1800"
#   0x00001458: mov rbx, qword ptr [rsp + 4] - [ 48 8b 5c 24 04 ]
#   0x0000145d: cmp rbx, qword ptr [r10] - [ 49 3b 1a ]
#   0x00001460: jne .return_addr_compromised - [ 0f 85 9a 03 00 00 ]
#   0x00001466: sub r10, 4 - [ 49 83 ea 04 ]
#   0x0000146a: ret - [ c3 ]

Флаг --address задает стартовый адрес для ассемблирования, что видно по адресу первой инструкции в выводе Nyxstone. С помощью флага --labels можно определять произвольные метки. Несколько меток задаются как пары "ключ-значение", разделенные запятыми, например: --labels "label0=0x1000,label1=0x1200".

При ассемблировании инструкций, относительных счетчика программы, Nyxstone учитывает как позицию в коде, так и адреса предоставленных меток, обеспечивая корректные результаты.

Архитектура и конфигурация​

Nyxstone предоставляет гибкие опции для настройки под разные системы, показывая возможности конфигурации архитектуры и специфичных для ISA расширений.

По умолчанию Nyxstone использует архитектуру x86_64, но может быть настроен на любую архитектуру, поддерживаемую связанной библиотекой LLVM, через флаг --arch. Входными данными может быть либо LLVM triple для архитектуры, либо ее сокращение. Это позволяет ассемблировать инструкции для различных архитектур, таких как armv8m, armv8-thumb, aarch64, riscv32 и других:
Код: Скопировать в буфер обмена
Код:
$ ./nyxstone --arch "armv8m" -A "add r0, r1"
#   0x00000000: add r0, r0, r1 - [ 01 00 80 e0 ]

$ ./nyxstone --arch "thumb8" -A "add r0, r1"
#   0x00000000: add r0, r1 - [ 08 44 ]

$ ./nyxstone --arch "aarch64" -A "add w0, w1, w2"
#   0x00000000: add w0, w1, w2 - [ 20 00 02 0b ]

$ ./nyxstone --arch "riscv32" -A "add t0, t1, zero"
#   0x00000000: add t0, t1, zero - [ b3 02 03 00 ]

Расширения архитектуры по умолчанию не включены в LLVM, так как предполагается, что реализован только базовый ISA. Поэтому, например, Nyxstone не может ассемблировать инструкцию с плавающей точкой на armv8m в базовой конфигурации:
Код: Скопировать в буфер обмена
Код:
$ ./nyxstone --arch "armv8m" -A "vadd.f32 s0, s1, s2"
# Could not assemble vadd.f32 s0, s0, s1 (= Error during assembly: error: instruction requires: VFP2
# vadd.f32 s0, s0, s1
# ^
# )

Здесь пригодятся дополнительные опции конфигурации LLVM. Можно указать точную модель процессора, чтобы включить его стандартные функции, или активировать конкретные расширения ISA для заданной архитектуры набора инструкций. Для приведенной инструкции с плавающей точкой можно указать процессор как Cortex-M7 (который имеет расширение ARM для операций с плавающей точкой) или включить конкретную функцию для требуемого расширения.

Сначала укажем точную модель процессора:
Код: Скопировать в буфер обмена
Код:
$ ./nyxstone --arch "armv8m" --cpu "cortex-m7" -A "vadd.f32 s0, s1, s2"
#   0x00000000: vadd.f32 s0, s0, s1 - [ 30 ee 20 0a ]

Другой вариант - включить конкретное расширение ISA. Функции указываются через запятую, каждое значение предваряется плюсом (+) или минусом (-) в зависимости от того, надо ли включить или отключить функцию. В этом случае Nyxstone через LLVM сообщает о необходимой функции vfp2:
Код: Скопировать в буфер обмена
Код:
$ ./nyxstone --arch "armv8m" --features "+vfp2" -A "vadd.f32 s0, s0, s1"
#   0x00000000: vadd.f32 s0, s0, s1 - [ 20 0a 30 ee ]

Nyxstone в качестве библиотеки​

Хоть и CLI - отличный инструмент для повседневного ассемблирования или дизассемблирования инструкций, Nyxstone в своей основе является библиотекой. Nyxstone написан на C++ для взаимодействия с LLVM и, соответственно, имеет C++ API.

Также реализованы биндинги для Rust, так как этот язык обеспечивает безопасность работы с памятью и используется для создания технологии перезаписи бинарников Emproof Nyx.

Кроме того, для быстрого и простого использования Nyxstone многими людьми, были реализованы биндинги для Python.

Использование Nyxstone в качестве библиотеки обычно довольно просто. Основное требование для сборки и линковки Nyxstone - установленные на системе библиотеки LLVM версии 15, при этом соответствующий llvm-config должен быть либо в пути, либо нужно указать расположение библиотек LLVM через переменную окружения NYXSTONE_LLVM_PREFIX.

Если указан путь к LLVM, Nyxstone будет искать llvm-config по пути $NYXSTONE_LLVM_PREFIX/bin/llvm-config.

Дополнительные инструкции по сборке и требуемые пакеты документированы в GitHub [README].

C++ использование​

Прямой способ взаимодействия с Nyxstone - через C++. Для линковки Nyxstone и сборки CLI в качестве примера кода для C++ API используется CMake. Пример интеграции Nyxstone в проект с помощью CMake приведен в README Nyxstone.

Также возможно использовать Nyxstone без CMake; Makefile, который использовался до перехода на CMake, можно найти в репозитории.

Основные моменты использования C++ API Nyxstone, можно найти здесь.

Использование в Rust​

Биндинги Nyxstone для Rust являются приоритетными. Цель для биндингов Rust и Python - соответствие особенностям каждого языка, используя их специфические возможности.

Вот пример использования Nyxstone из Rust. В нем создается экземпляр Nyxstone. Он настроен на использование шестнадцатеричного представления для непосредственных значений в дизассемблированном коде и в остальном использует стандартную конфигурацию. Затем он используется для ассемблирования jne .label в объект Instruction, который содержит не только машинный код, но и адрес, и ассемблерное представление каждой инструкции. Наконец, он используется для дизассемблирования байтов инструкции 0x31 0xd8 в читаемую инструкцию.

Код: Скопировать в буфер обмена
Код:
use std::collections::HashMap;
use anyhow::Result;
use nyxstone::{IntegerBase, Nyxstone, NyxstoneConfig, Instruction};

fn main() -> Result<()> {
    let nyxstone = Nyxstone::new(
        "x86_64",
        NyxstoneConfig {
            immediate_style: IntegerBase::HexPrefix,
            ..Default::default()
        },
    )?;

    // Можно также ассемблировать в [`u8`], используя `assemble()`
    let instructions = nyxstone.assemble_to_instructions(
        "jne .label",
        0x1000,
        &HashMap::from([(".label", 0x1200)]),
    )?;

    assert_eq!(
        instructions,
        vec![Instruction {
            address: 0x1000,
            assembly: "jne .label",
            bytes: vec![0x0f, 0x85, 0xfa, 0x01, 0x00, 0x00]
        }]
    );

    // Можно также дизассемблировать в объекты [`Instruction`], используя `disassemble_to_instructions()`
    let disassembly = nyxstone.disassemble(
        &[0x31, 0xd8],
        /* address= */ 0x0,
        // Количество инструкций для дизассемблирования (0 = все)
        /* count = */ 0,
    )?;

    assert_eq!(disassembly, "xor eax, ebx\n".to_owned());

    Ok(())
}

Nyxstone опубликован как крейт на crates.io. Чтобы использовать Nyxstone в своем Rust-проекте, достаточно добавить его в Cargo.toml. Но надо убедиться, что LLVM 15 установлен и llvm-config доступен через переменную окружения или в системном пути; иначе Nyxstone не сможет корректно слинковаться.

Использование в Python​

Для удобства экспериментов также предоставляются биндинги для Python с использованием pybind11.

Вот пример использования Nyxstone из Python, где создается экземпляр Nyxstone с определенными настройками для CPU и стиля непосредственных значений. Затем демонстрируется, как ассемблировать и дизассемблировать некоторые инструкции с помощью этого экземпляра.
Python: Скопировать в буфер обмена
Код:
from nyxstone import Nyxstone, IntegerBase, Instruction

# Создание экземпляра Nyxstone для x86_64 с использованием CPU corei7 и шестнадцатеричным стилем вывода непосредственных значений
nyxstone = Nyxstone("x86_64", cpu="corei7", immediate_style=IntegerBase.HexPrefix)

# Ассемблирование `xor rax, rax` в байты, представленные списком целых чисел
nyxstone.assemble("xor rax, rax")
# = [0x48, 0x31, 0xc0]

# Ассемблирование `jmp label` в информацию об инструкции, содержащую адрес, ассемблерный код и байты машинного кода
nyxstone.assemble_to_instructions("jmp label", address=0x1000, labels={"label": 0x1080})
# = [Instruction(address=0x1000, assembly="jmp label", bytes=[0xeb, 0x7e])]

# Дизассемблирование `0x13 0x37` в строку
nyxstone.disassemble([0x13, 0x37])
# 'adc esi, dword ptr [rdi]\n'

Nyxstone для Python публикуется через PyPI. Его можно установить из PyPI командой pip install nyxstone или из исходников, используя pip install . в поддиректории с python-биндингами.

Заключение​

В этой статье был представлен открытый фреймворк для ассемблирования и дизассемблирования Nyxstone, построенный на базе LLVM. Он предлагает широкую поддержку конфигурации архитектур и их возможностей, точное сообщение об ошибках и способность определять произвольные метки при ассемблировании. Nyxstone доступен через API для C++, Rust и Python, а также включает универсальный инструмент командной строки.

В дорожной карте проекта можно ознакомиться с планируемыми функциями.

Переведено специально для XSS.is
Автор перевода: ordinaria1
Источник: www.emproof.com/introducing-nyxstone-an-llvm-based-disassembly-framework
 
Сверху Снизу