0x42
Форумчанин
- Регистрация
- 05.05.2025
- Сообщения
- 90
- Реакции
- 40

Введение
syscalls-cpp — это заголовочная библиотека на C++20, реализующая политику-ориентированный фреймворк для выполнения прямых системных вызовов в окружениях x86/x64 под Windows.
Она предоставляет модульную архитектуру, где вы можете на этапе компиляции комбинировать различные стратегии выделения памяти и генерации stub-функций для достижения оптимального баланса между безопасностью и производительностью.
Библиотека автоматически определяет номера системных вызовов, напрямую парся метаданные ntdll.dll: на x64 — через структуру exception directory, а на x86 — сортируя экспортированные функции по адресам.
Такой подход устойчив к пользовательским хукам и позволяет находить даже «соседние» вызовы, если оригинал был подменён или патчен.
Основные компоненты (политики)
Все политики разделены на три группы: выделение памяти (Allocation), генерация stub-функций (Stub Generation) и разбор метаданных (Parsing).
Любую политику аллокации можно сочетать с любой политикой генерации и разбора.
1. Политики выделения памяти
allocator::section — вызывает NtCreateSection с флагом SEC_NO_CHANGE;
allocator::heap — создает секцию через HeapCreate с HEAP_CREATE_ENABLE_EXECUTE;
allocator::memory — использует NtAllocateVirtualMemory (RW → RX).
2. Политики генерации stub-функций
generator::direct — классическая инструкция syscall внутри самостоятельного кода;
generator::gadget (только x64) — прыжок на найденный в ntdll.dll gadget «syscall; ret»;
generator::exception (только x64) — генерирует UD2 и обрабатывает его через векторный обработчик исключений (VEH).
3. Политики разбора метаданных системных вызовов
[*]parser::directory — на x64 связывает exception directory (.pdata) с таблицей экспорта, на x86 сортирует Zw функции по адресу;
[*]parser::signature — сканирует пролога функций на подпись «mov r10, rcx; mov eax, syscall_id» с учётом защиты от хуков.
Установка
Через vcpkg:
Код:
vcpkg install syscalls-cpp
Интеграция с MSBuild и CMake происходит автоматически.
Ручная настройка:
Клонируйте репозиторий и добавьте папку include в пути компиляции.
Пример использования
Стандартный сценарий
C++:
#include <iostream>
#include <syscalls-cpp/syscall.hpp>
int main()
{
syscall::Manager<syscall::policies::allocator::section, syscall::policies::generator::direct> syscallManager;
if (!syscallManager.initialize())
{
std::cerr << "initialization failed!\n";
return 1;
}
PVOID pBaseAddress = nullptr;
SIZE_T uSize = 0x1000;
syscallManager.invoke<NTSTATUS>(
SYSCALL_ID("NtAllocateVirtualMemory"),
syscall::native::getCurrentProcess(),
&pBaseAddress,
0, &uSize,
MEM_COMMIT | MEM_RESERVE,
PAGE_READWRITE
);
if (pBaseAddress)
std::cout << "allocation successful at 0x" << pBaseAddress << std::endl;
return 0;
}
Расширенный сценарий:
В стандартном сценарии просто получение и вызов системного вызова.
В следующим ниже фрагменте кода реализуется динамическая генерация «обфусцированного» стаба (stub) для прямого вызова системного вызова с помощью политики EncryptedShellGenerator:
C++:
#pragma warning(disable : 4146) // neg for unsigned
#include <syscalls-cpp/syscall.hpp>
#include <iostream>
#include <cstdint>
#include <array>
#include <vector>
#include <random>
#include <functional>
#include <chrono>
#include <initializer_list>
#include <cstring>
class CBufferWriter
{
public:
CBufferWriter(uint8_t* buffer, size_t size) : m_pStart(buffer), m_pCurrent(buffer), m_pEnd(buffer + size)
{
}
template<typename T>
void write(T value)
{
if (m_pCurrent + sizeof(T) <= m_pEnd)
{
*reinterpret_cast<T*>(m_pCurrent) = value;
m_pCurrent += sizeof(T);
}
}
void writeBytes(std::initializer_list<uint8_t> listBytes) {
if (m_pCurrent + listBytes.size() <= m_pEnd)
{
memcpy(m_pCurrent, listBytes.begin(), listBytes.size());
m_pCurrent += listBytes.size();
}
}
size_t getCurrentSize() const {
return m_pCurrent - m_pStart;
}
void fillRest(uint8_t uValue)
{
if (m_pCurrent < m_pEnd) {
memset(m_pCurrent, uValue, m_pEnd - m_pCurrent);
}
}
private:
uint8_t* m_pStart;
uint8_t* m_pCurrent;
const uint8_t* m_pEnd;
};
struct EncryptedShellGenerator
{
static constexpr bool bRequiresGadget = false;
static constexpr size_t kMaxStubSize = 128;
private:
enum class EOperationType { ADD, SUB, XOR, NOT, NEG, ROL, ROR };
struct ObfuscationLayer_t
{
EOperationType m_eType;
uint32_t m_uKey;
};
inline static std::mt19937 randEngine{ static_cast<unsigned int>(std::chrono::high_resolution_clock::now().time_since_epoch().count()) };
public:
static void generate(uint8_t* pBuffer, uint32_t uSyscallNumber, void* /*pGadgetAddress*/)
{
CBufferWriter writer(pBuffer, kMaxStubSize);
std::uniform_int_distribution<> operationDist(0, 6);
std::uniform_int_distribution<uint32_t> keyDist(1, 0xFFFFFFFF);
std::uniform_int_distribution<> layersDist(4, 8);
const int iAmountOfLayers = layersDist(randEngine);
std::vector<ObfuscationLayer_t> vecLayers;
vecLayers.reserve(iAmountOfLayers);
for (int i = 0; i < iAmountOfLayers; ++i)
{
EOperationType operationType = static_cast<EOperationType>(operationDist(randEngine));
uint32_t uKey = 0;
if (operationType == EOperationType::ROL || operationType == EOperationType::ROR)
uKey = keyDist(randEngine) % 31 + 1;
else if (operationType != EOperationType::NOT && operationType != EOperationType::NEG)
{
uKey = keyDist(randEngine);
if (uKey == 0)
uKey = 1;
}
vecLayers.push_back({ operationType, uKey });
}
uint32_t uEncryptedValue = uSyscallNumber;
for (auto it = vecLayers.rbegin(); it != vecLayers.rend(); ++it)
{
const auto& layer = *it;
switch (layer.m_eType)
{
case EOperationType::ADD: uEncryptedValue -= layer.m_uKey; break;
case EOperationType::SUB: uEncryptedValue += layer.m_uKey; break;
case EOperationType::XOR: uEncryptedValue ^= layer.m_uKey; break;
case EOperationType::NOT: uEncryptedValue = ~uEncryptedValue; break;
case EOperationType::NEG: uEncryptedValue = -uEncryptedValue; break;
case EOperationType::ROL: uEncryptedValue = (uEncryptedValue >> layer.m_uKey) | (uEncryptedValue << (32 - layer.m_uKey)); break; // ROR
case EOperationType::ROR: uEncryptedValue = (uEncryptedValue << layer.m_uKey) | (uEncryptedValue >> (32 - layer.m_uKey)); break; // ROL
}
}
writer.writeBytes({ 0x51, 0x41, 0x5A }); // push rcx; pop r10
writer.write<uint8_t>(0xB8); // mov eax, ...
writer.write<uint32_t>(uEncryptedValue); // ... encryptedValue
emitJunk(writer);
for (const auto& layer : vecLayers)
{
switch (layer.m_eType) {
case EOperationType::ADD:
writer.write<uint8_t>(0x05); // add eax, key
writer.write<uint32_t>(layer.m_uKey);
break;
case EOperationType::SUB:
writer.write<uint8_t>(0x2D); // sub eax, key
writer.write<uint32_t>(layer.m_uKey);
break;
case EOperationType::XOR:
writer.write<uint8_t>(0x35); // xor eax, key
writer.write<uint32_t>(layer.m_uKey);
break;
case EOperationType::NOT:
writer.writeBytes({ 0xF7, 0xD0 }); // not eax
break;
case EOperationType::NEG:
writer.writeBytes({ 0xF7, 0xD8 }); // neg eax
break;
case EOperationType::ROL:
writer.writeBytes({ 0xC1, 0xC0 }); // rol eax, key
writer.write<uint8_t>(static_cast<uint8_t>(layer.m_uKey));
break;
case EOperationType::ROR:
writer.writeBytes({ 0xC1, 0xC8 }); // ror eax, key
writer.write<uint8_t>(static_cast<uint8_t>(layer.m_uKey));
break;
}
emitJunk(writer);
}
writer.writeBytes({ 0x0F, 0x05 }); // syscall
writer.writeBytes({ 0x48, 0x83, 0xC4, 0x08 }); // add rsp 8
writer.writeBytes({ 0xFF, 0x64, 0x24, 0xF8 }); // jmp qword ptr [rsp-8]
writer.fillRest(0xCC); // int3
}
static constexpr size_t getStubSize() { return kMaxStubSize; }
private:
static void emitJunk(CBufferWriter& writer)
{
std::uniform_int_distribution<> junkDist(0, 5);
int iJunkType = junkDist(randEngine);
const uint8_t uREXW = 0x48;
const std::array<uint8_t, 6> arrPushOpcodes = { 0x50, 0x51, 0x52, 0x53, 0x56, 0x57 };
const std::array<uint8_t, 6> arrPopOpcodes = { 0x58, 0x59, 0x5A, 0x5B, 0x5E, 0x5F };
const std::array<uint8_t, 6> arrIncDecNegModRM = { 0xC0, 0xC1, 0xC2, 0xC3, 0xC6, 0xC7 };
const std::array<uint8_t, 6> arrLeaModRM = { 0x40, 0x49, 0x52, 0x5B, 0x76, 0x7F };
std::uniform_int_distribution<> regDist(0, arrPushOpcodes.size() - 1);
int iRegIdx = regDist(randEngine);
switch (iJunkType)
{
case 0:
// nop
writer.write<uint8_t>(0x90);
break;
case 1:
// push rXX; pop rXX
writer.write<uint8_t>(arrPushOpcodes[iRegIdx]);
writer.write<uint8_t>(arrPopOpcodes[iRegIdx]);
break;
case 2:
// pushfq
writer.write<uint8_t>(0x9C);
// inc rXX
writer.writeBytes({ uREXW, 0xFF, arrIncDecNegModRM[iRegIdx] });
// dec rXX
writer.writeBytes({ uREXW, 0xFF, (uint8_t)(arrIncDecNegModRM[iRegIdx] + 0x08) });
// popfq
writer.write<uint8_t>(0x9D);
break;
case 3:
// lea rXX, [rXX + 0x00]
writer.writeBytes({ uREXW, 0x8D, arrLeaModRM[iRegIdx], 0x00 });
break;
case 4:
{
// pushfq
writer.write<uint8_t>(0x9C);
std::uniform_int_distribution<uint32_t> valDist;
uint32_t uRandomVal = valDist(randEngine);
// xor rXX, imm32
writer.writeBytes({ uREXW, 0x81, (uint8_t)(0xF0 + iRegIdx) });
writer.write<uint32_t>(uRandomVal);
// xor rXX, imm32
writer.writeBytes({ uREXW, 0x81, (uint8_t)(0xF0 + iRegIdx) });
writer.write<uint32_t>(uRandomVal);
// popfq
writer.write<uint8_t>(0x9D);
}
break;
case 5:
break;
}
}
};
int main()
{
syscall::Manager < syscall::policies::allocator::section, EncryptedShellGenerator> syscallManager;
if (!syscallManager.initialize())
{
std::cerr << "failed to initialize syscall manager" << std::endl;
return 1;
}
std::cout << "syscall manager initialized successfully" << std::endl;
NTSTATUS status;
PVOID pBaseAddress = nullptr;
SIZE_T uSize = 0x1000;
status = syscallManager.invoke<NTSTATUS>(
SYSCALL_ID("NtAllocateVirtualMemory"),
syscall::native::getCurrentProcess(),
&pBaseAddress,
0,
&uSize,
MEM_COMMIT | MEM_RESERVE,
PAGE_READWRITE
);
if(pBaseAddress)
std::cout << "memory allocated: " << pBaseAddress;
else
std::cout << "failed to allocate, status:" << status;
return 0;
}
Структура EncryptedShellGenerator
Это политика генерации стаба для
syscall::Manager
.Она умеет:
- Случайно генерировать цепочку из 4–8 обфускационных «слоёв» (операций
ADD
,SUB
,XOR
,NOT
,NEG
,ROL
,ROR
), каждая с собственным 32-битным ключом или сдвигом. - Предварительно «зашифровывать» номер системного вызова путём последовательного применения обратных операций всех слоёв (чтобы в самом стубе сначала выполнить дешифровку).
- Записывать в буфер:
- Пролог для переноса
rcx
→r10
(push rcx; pop r10
) — требуется для корректной работыsyscall
на x64. - Инструкцию
mov eax, <encryptedValue>
(Мнемоника0xB8 …
) со «зашифрованным» номером. - Случайный «мусор» (
emitJunk
) — одна из пяти вариаций, чтобы шумить статический анализ в памяти. - Блок расшифровки: для каждого слоя по очереди генерируется нужная машинная инструкция (
add eax, key
,sub eax, key
,xor eax, key
,not eax
,neg eax
,rol eax, imm8
,ror eax, imm8
), с чередованием мусора. - Наконец, сам
syscall
(0x0F 0x05
), корректное выравнивание стека (add rsp, 8
) и переход по адресу возврата (jmp qword ptr [rsp-8]
). - Паддинг до максимального размера стуба (
kMaxStubSize = 128 байт
) инструкциямиint3
(0xCC
). Т
- Пролог для переноса
Функция emitJunk
Вставляет один из нескольких видов «бесполезных» инструкций (NOP, push/pop регистров, манипуляции флагами и т. п.), чтобы ещё больше запутать дизассемблированный код.
Основная функция
main
Код:
syscall::Manager<syscall::policies::allocator::section,
EncryptedShellGenerator> syscallManager;
if (!syscallManager.initialize()) { … }
syscallManager.invoke<NTSTATUS>(
SYSCALL_ID("NtAllocateVirtualMemory"),
syscall::native::getCurrentProcess(), &pBaseAddress, 0, &uSize,
MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE
);
Здесь создаётся менеджер системных вызовов, который:
- Использует политику allocator::section (создаёт исполняемую секцию через NtCreateSection).
- Для генерации каждого стаба применяет политику EncryptedShellGenerator.
После инициализации он напрямую вызывает NtAllocateVirtualMemory, указав параметры для резервирования и коммита памяти в текущем процессе.
Репозиторй, где можно скачать фреймворк:GitHub - sapdragon/syscalls-cpp: A modern C++20 header-only library for advanced direct system call invocation.
Последнее редактирование: