
Библиотека, упрощающая использование indirect syscalls. Довольно интересный обход AV/EDR в качестве PoC.
Этот проект реализует удобный и современный способ использования функций NTAPI через непрямые системные вызовы (indirect syscalls) в сочетании с методом FreshyCalls с небольшой доработкой для динамического получения номеров системных вызовов. Также здесь используется техника, о которой я не видел упоминаний, для обхода сканирования памяти Windows Defender. Проект реализует классический PoC-инжектор процессов, использующий эту библиотеку. Доступны и удобные функции для шифрования.

Чтобы собрать пример, используйте -DNULLGATE_BUILD_SAMPLE=ON.
Если вы собрали nullgate напрямую, он будет доступен по пути <build_dir>/sample.exe; если вы собрали его как зависимость, то по пути <build_dir>/_deps/nullgate-build/sample.exe.
В Windows из-за странных путей сборки он, скорее всего, будет находиться в тех же базовых каталогах, где и примеры, но, вероятно, вложен на несколько уровней глубже.
В качестве аргумента он принимает PID процесса, в который вы хотите внедрить шелл-код.
[!WARNING] Если вы используете Linux, вам необходимо установить кросс-компилятор mingw. Например, на Arch можно выполнить
pacman -S mingw-w64-gcc. Затем используйте опцию-DNULLGATE_CROSSCOMPILE=ON, чтобы установить mingw в качестве компилятора по умолчанию.
[!TIP] Также рекомендуется выполнить strip результирующего бинарника, чтобы снизить вероятность обнаружения.
git clone https://github.com/0xsch1zo/NullGate
cd NullGate
cmake . -B build -DNULLGATE_BUILD_SAMPLE=ON
cmake --build build/
Его можно собрать с помощью флага -DNULLGATE_DEPRECATED_HASHER.
Поддерживается CMake FetchContent. Вот пример простого CMakeLists.txt:
cmake_minimum_required(VERSION 3.25)
include(FetchContent)
FetchContent_Declare(nullgate
GIT_REPOSITORY https://github.com/0xsch1zo/NullGate
GIT_TAG 1.2.0
)
FetchContent_MakeAvailable(nullgate)
project(test)
add_executable(test
main.cpp
)
target_link_libraries(test
PRIVATE nullgate
)
Линковка выполняется статически, поэтому вам не нужно беспокоиться о видимости символов.
[!NOTE] В следующих примерах будет использоваться
namespace ng = nullgate
Использование довольно простое. Вот фрагмент, демонстрирующий основную функциональность:
ng::syscalls syscalls;
typedef NTSTATUS NTAPI NtAllocateVirtualMemory(
_In_ HANDLE ProcessHandle,
_Inout_ _At_(*BaseAddress,
_Readable_bytes_(*RegionSize) _Writable_bytes_(*RegionSize)
_Post_readable_byte_size_(*RegionSize)) PVOID *BaseAddress,
_In_ ULONG_PTR ZeroBits, _Inout_ PSIZE_T RegionSize,
_In_ ULONG AllocationType, _In_ ULONG PageProtection);
NTSTATUS status = syscalls.SCall<NtAllocateVirtualMemory>(
ng::obfuscation::fnv1Const("NtAllocateVirtualMemory"), processHandle,
&buf, 0, ®ionSize, MEM_RESERVE | MEM_COMMIT, PAGE_EXECUTE_READWRITE);
Встроена типобезопасность. Вам просто нужно предоставить определение NT-функции, которую вы хотите вызвать! Его легко получить с ntdoc. Это рекомендуемый способ использования библиотеки.
Для тех, кому не нравится чёрная магия шаблонов C++ или что-то подобное, предыдущий интерфейс всё ещё доступен:
NTSTATUS status = syscalls.Call(ng::obfuscation::fnv1Const("NtAllocateVirtualMemory"),
processHandle, (PVOID)&buf, (ULONG_PTR)0, ®ionSize,
(ULONG)(MEM_RESERVE | MEM_COMMIT), (ULONG)PAGE_EXECUTE_READWRITE);
При использовании этого интерфейса вам нужно приводить аргументы к правильному типу; если этого не делать, могут возникнуть проблемы.
constexpr uint64_t hash = ng::obfuscation::fnv1Const("NtAllocateVirtualMemory");
Продемонстрированный ранее метод fnv1Const приносит радости современного C++ в мир разработки вредоносного ПО. Это consteval-функция, поэтому гарантируется, что она будет вычислена на этапе компиляции, заменяя читаемое имя функции на FNV1-хеш.
Существует также runtime-эквивалент под названием fnv1Runtime, но, конечно, он не даёт преимущества запутывания имён наших функций. Он используется реализацией, чтобы определить, для какой функции внутри ntdll получать номер системного вызова.
Есть три процедуры для XOR-«шифрования»:
constexpr ng::obfuscation::ConstData xored = ng::obfuscation::xorConst("some string");
std::cout << xored.string();
Возможный вывод:
<Y:-E&X0
Эта процедура, как и функция fnv1Const, вычисляется на этапе компиляции, поэтому "some string" никогда не появится в бинарнике, поскольку строка будет храниться в зашифрованном виде.
Но, эй, нам ведь нужно реально использовать эти данные! ConstData — это тонкая обёртка над сырыми байтами, имеющая постоянный размер.
У неё есть два метода: raw() и string().
raw() возвращает std::vector<unsigned char>, а string, как следует из названия, возвращает std::string.
[!NOTE] Если вам нужно сконструировать
ConstDataнапрямую из строкового литерала, используйтеstd::to_arrayдля создания промежуточного массива, передаваемого вConstData. Однако учтите, что при таком подходеConstDataбудет хранить дополнительный нулевой символ литерала.
Конечно, нам нужен способ расшифровать это и использовать — это следующая функция, которую мы рассмотрим.
xorRuntime — вторая доступная процедура.
Как следует из названия, это runtime-эквивалент xorConst.
Обратите внимание, что его можно использовать не только для расшифровки — он работает в обе стороны, хотя использование его для шифрования не даёт преимуществ запутывания строки на этапе компиляции.
ng::obfuscation::ConstData xored = ng::obfuscation::xorConst("some string");
ng::obfuscation::ConstData data = ng::obfuscation::xorRuntime(xored);
assert(data.string() == "some string");
std::string dynamicString = "some data of dynamic size";
auto dynamicData = ng::obfuscation::DynamicData(dynamicString);
auto xoredDynamic = ng::obfuscation::xorRuntime(dynamicData);
auto unxoredDynamic = ng::obfuscation::xorRuntime(xoredDynamic);
assert(unxoredDynamic.string() == dynamicString);
DynamicData имеет те же методы, что и ConstData, с добавленными конструкторами для совместимости и, как следует из названия, может иметь размер, неизвестный на этапе компиляции.
xorRuntime принимает как DynamicData, так и ConstData.
Хотя мы всегда могли бы расшифровывать данные в рантайме, сначала вызывая xorConst и передавая результат в xorRuntime, это довольно утомительно.
И вот решение этой проблемы — вишенка на торте: xorRuntimeDecrypted. Это удобная функция для строковых литералов, с которыми не нужно работать, пока они находятся в зашифрованном состоянии.
Она шифрует строковый литерал на этапе компиляции, а затем расшифровывает его в рантайме одним вызовом. Разве не круто!.
ng::obfuscation::ConstData text = ng::obfuscation::xorRuntimeDecrypted<"some string">();
assert(data.string() == "some string");
Кто-то может сказать: всё здорово, но где ключ? В nullgate 1.2 ключ генерируется случайным образом при каждой новой сборке (то есть при каждом запуске команды cmake). Это ещё сильнее снижает шанс быть обнаруженным по сигнатурам.
Суть проблемы в том, что при вызове NtCreateRemoteThreadEx или NtCreateProcess запускается сканирование памяти, и наш до чёртиков сигнатурный msfvenom-пейлоад обнаруживается.
Известное решение — сначала при вызове NtAllocateVirtualMemory установить права страницы как PAGE_NOACCESS, а затем создать поток в приостановленном состоянии.
Когда Windows Defender попытается просканировать память нашего процесса, у него это не получится.
Затем мы можем возобновить выполнение потока с помощью NtResumeThread.
Это работает, но что если используется более компетентное защитное решение? Что бы оно сделало?
Оно, конечно, просто использовало бы VirtualProtect, чтобы изменить права нашей страницы, и обнаружило бы msfvenom.
Чтобы обойти это, я немного изменил стратегию. Вместо установки страницы как PAGE_NOACCESS при первой записи в память процесса мы можем просто записать в процесс какие-то мусорные данные (Да, это обязательно, или же я просто слишком глуп, чтобы найти способ обойтись без этого).
Затем мы создаём поток в приостановленном состоянии.
После этого мы записываем в процесс нужный шелл-код и, наконец, возобновляем поток с помощью NtResumeThread.
Благодаря этому методу нам не нужно беспокоиться о том, что к нашей памяти обратятся после вызова NtCreateThreadEx, потому что в ней ничего нет.
Только после этого записывается расшифрованный шелл-код, и выполнение возобновляется.
Эта библиотека создана исключительно в академических целях. Авторы не несут ответственности за то, что делается с помощью этой библиотеки, и поэтому мы освобождаемся от любой ответственности, возникающей в результате её неправомерного использования.