• XSS.stack #1 – первый литературный журнал от юзеров форума

syscalls-cpp: гибкий фреймворк для вызова syscall'ов

sapdragon

floppy-диск
Пользователь
Регистрация
09.06.2025
Сообщения
7
Реакции
13
Да, это очередная библиотека для системных вызовов, но эта необычная. Это C++20 фреймворк на основе заголовочных файлов с политико-ориентированным дизайном. Это не черный ящик - вы собираете свою стратегию syscall'ов на этапе компиляции.
Можно миксовать политики для:
Распределения памяти:
  • HeapAllocator: Хранит стабы в приватной исполняемой куче
  • SectionAllocator: Использует секцию с SEC_NO_CHANGE (не перезаписываемая без ремаппинга)
  • VirtualMemoryAllocator: Обычный NtAllocateVirtualMemory (RW -> RX)
Генерации стабов:
  • DirectStubGenerator: Классический автономный syscall-стаб
  • GadgetStubGenerator: Прыгает на syscall; ret гаджет в ntdll.dll
  • ExceptionStubGenerator: Триггерит breakpoint (ud2) для syscall'а через кастомный VEH
Парсинга SSN:
  • ExceptionDirectoryParser: Парсит PE exception directory (.pdata секцию)
  • SignatureScanningParser: Сканит прологи функций на mov r10, rcx; mov eax, syscall_id с детектом хуков
Пример использования:
C++:
#include "syscall.hpp"

using MyStealthyManager = syscall::Manager< 
    syscall::policies::SectionAllocator,    // стабы в защищенной секции 
    syscall::policies::GadgetStubGenerator  // через гаджет
>;

int main() { 
    MyStealthyManager syscalls; 
    if (!syscalls.initialize())     
        return 1;   
  
    syscalls.invoke<NTSTATUS>(SYSCALL_ID("NtAllocateVirtualMemory"), /* аргументы */);   
  
    return 0;
}
Ограничения:
  • Только x64
  • Пока нет stack spoofing (детектится instrumentation callbacks)
  • Нет No-CRT версии ( пока что )
Буду допиливать. Фидбек приветствуется.

GitHub: https://github.com/sapdragon/syscalls-cpp
 
Не понял для чего там функцинал создания секций и тд, не вникал в исключения

по поводу сисколов - парсишь ты код нт функций, там могут стоять хуки, ты их обрабатываешь как то - но зачем?
просто сортируй по rva и у тебя будут все сисколы по порядку

сискол помоему занимает не все 4 байта, а лишь 12 бит
остальное: номер таблицы, турбозунки, и без них можно вызывать апи - просто по номеру сискола (турбозунки нужны для оптимизации)
 
Не понял для чего там функцинал создания секций и тд, не вникал в исключения

по поводу сисколов - парсишь ты код нт функций, там могут стоять хуки, ты их обрабатываешь как то - но зачем?
просто сортируй по rva и у тебя будут все сисколы по порядку

сискол помоему занимает не все 4 байта, а лишь 12 бит
остальное: номер таблицы, турбозунки, и без них можно вызывать апи - просто по номеру сискола (турбозунки нужны для оптимизации)
Буквально описано зачем там секции ( нельзя будет писать без ремаппинга!! ).
по поводу сисколов - парсишь ты код нт функций, там могут стоять хуки, ты их обрабатываешь как то - но зачем?
просто сортируй по rva и у тебя будут все сисколы по порядку

Я не знаю, чем ты читал код, но я это и делаю в ExceptionDirectoryParser ( основной парсер ), а то что ты описываешь фаллбек SignatureScanningParser
сискол помоему занимает не все 4 байта, а лишь 12 бит
это попытка в педантизм или как? к чему данная информация?
турбозунки нужны для оптимизации
покажи пожауйста вызов какого-нибудь NtCreateFile без так называемого "турбозунка"

В целом ни одного аргумента не понял, сообщение, чтобы что?
 
Последнее редактирование:
Пожалуйста, обратите внимание, что пользователь заблокирован
Я не знаю, чем ты читал код, но я это и делаю в ExceptionDirectoryParser ( основной парсер ), а то что ты описываешь фаллбек SignatureScanningParser
Нет, то что он говорит, это сортировка Zw* по адресам, вообще не нужно ни проверять хук, не извлекать номер сисколла из кода. Перечисли все Zw* функции, отсортируй их по адресам, индекс в сортированном массиве - это?

В целом ни одного аргумента не понял, сообщение, чтобы что?
Тебе нужно больше гулять, трогать траву.
 
Я не знаю, чем ты читал код, но я это и делаю в ExceptionDirectoryParser ( основной парсер ), а то что ты описываешь фаллбек SignatureScanningParser
х#йню ты какуюто делаешь


это попытка в педантизм или как? к чему данная информация?
К тому что парсинг через сигнатуры и сортировка - дают разные значения(дворды) сискола
А разный результат в свою очередь и обосновывается тем что я сказал
Хотя что я рассказываю? Хуле - меньше знаешь крепче спишь


покажи пожауйста вызов какого-нибудь NtCreateFile без так называемого "турбозунка"
Что подсказывать то, в регистр индекс Nt* функции, на стек параметры и все


В целом ни одного аргумента не понял, сообщение, чтобы что?
Фидбек приветствуется.
 
Нет, то что он говорит, это сортировка Zw* по адресам, вообще не нужно ни проверять хук, не извлекать номер сисколла из кода. Перечисли все Zw* функции, отсортируй их по адресам, индекс в сортированном массиве - это?
Ладно, у вас на форуме не принято открывать исходный код, я кину скриншотом
1749916400381.png
 
х#йню ты какуюто делаешь



К тому что парсинг через сигнатуры и сортировка - дают разные значения(дворды) сискола
А разный результат в свою очередь и обосновывается тем что я сказал
Хотя что я рассказываю? Хуле - меньше знаешь крепче спишь
прошу открой исходный код ради бога, или ты мне предлагаешь в SignatureScanningParser сортировать еще? Там намеренно поиск по соседям, чтобы реимплентировать хало-гейтс
Про 12 бит к чему? У тебя при mov eax, syscallNumber и так верхние 32 бита очищаются, а дальше ядро всё правильно спарсит!!
Какие аргументы на стек, что? ты стдколл решил в х64 реимплементировать или как?

Я открыт для конструктивной критики, но она должна быть технически обоснованной. Твои замечания, к сожалению, таковыми не являются(
 
Последнее редактирование:
Пожалуйста, обратите внимание, что пользователь заблокирован
Ладно, у вас на форуме не принято открывать исходный код, я кину скриншотом
Больше всего меня в этом смешит, что ты сам зачем-то назвал export-директорию exception-директорией и удивляешься, что тебя никто не понял, ну окей, что.
 
Больше всего меня в этом смешит, что ты сам зачем-то назвал export-директорию exception-директорией и удивляешься, что тебя никто не понял, ну окей, что.
так все ссн считаются по директории exception... от экспортов строится только мапа для получения названия функции
1749921240552.png
 
Да, это очередная библиотека для системных вызовов, но эта необычная. Это C++20 фреймворк на основе заголовочных файлов с политико-ориентированным дизайном. Это не черный ящик - вы собираете свою стратегию syscall'ов на этапе компиляции.
Можно миксовать политики для:
Распределения памяти:
  • HeapAllocator: Хранит стабы в приватной исполняемой куче
  • SectionAllocator: Использует секцию с SEC_NO_CHANGE (не перезаписываемая без ремаппинга)
  • VirtualMemoryAllocator: Обычный NtAllocateVirtualMemory (RW -> RX)
Генерации стабов:
  • DirectStubGenerator: Классический автономный syscall-стаб
  • GadgetStubGenerator: Прыгает на syscall; ret гаджет в ntdll.dll
  • ExceptionStubGenerator: Триггерит breakpoint (ud2) для syscall'а через кастомный VEH
Парсинга SSN:
  • ExceptionDirectoryParser: Парсит PE exception directory (.pdata секцию)
  • SignatureScanningParser: Сканит прологи функций на mov r10, rcx; mov eax, syscall_id с детектом хуков
Пример использования:
C++:
#include "syscall.hpp"

using MyStealthyManager = syscall::Manager<
    syscall::policies::SectionAllocator,    // стабы в защищенной секции
    syscall::policies::GadgetStubGenerator  // через гаджет
>;

int main() {
    MyStealthyManager syscalls;
    if (!syscalls.initialize())    
        return 1;  
 
    syscalls.invoke<NTSTATUS>(SYSCALL_ID("NtAllocateVirtualMemory"), /* аргументы */);  
 
    return 0;
}
Ограничения:
  • Только x64
  • Пока нет stack spoofing (детектится instrumentation callbacks)
  • Нет No-CRT версии ( пока что )
Буду допиливать. Фидбек приветствуется.

GitHub: https://github.com/sapdragon/syscalls-cpp
Ты молодец мне нравится твой код
 
отличная либа, ничо не скажешь,

особенно понравился детект нопов которые (непонятно почему, хотелось бы узнать) иногда появляются по адресу экспортов на системах с установленными EDR

C++:
 private:
                static bool isFunctionHooked(const uint8_t* pFunctionStart)
                {
                    const uint8_t* pCurrent = pFunctionStart;

                    while (*pCurrent == 0x90)
                        pCurrent++;

                    switch (pCurrent[0])
                    {
                        // @note / SapDragon: JMP rel32
                    case 0xE9:
                        // @note / SapDragon: JMP rel8
                    case 0xEB:
                        // @note / SapDragon: push imm32
                    case 0x68:
                        return true;

                        // @note / SapDragon: jmp [mem] / jmp [rip + offset]
                    case 0xFF:
                        if (pCurrent[1] == 0x25)
                            return true;
                        break;

                        // @note / SapDragon: int3...
                    case 0xCC:
                        return true;

                        // @note / SapDragon: ud2
                    case 0x0F:
                        if (pCurrent[1] == 0x0B)
                            return true;
                        break;

                        // @note / SapDragon: int 0x3
                    case 0xCD:
                        if (pCurrent[1] == 0x03)
                            return true;
                        break;

                    default:
                        break;
                    }

                    return false;
                }
            };
        } // parsing
    } // policies

а вот тут вы и попалися.

C++:
if constexpr (platform::isWindows64)
                        {
                            if (isFunctionHooked(pFunctionStart) && !uSyscallNumber)
                            {
                                // @note / SapDragon: stable only on x64

                                // @note / SapDragon: search up
                                for (int j = 1; j < 20; ++j)
                                {
                                    uint8_t* pNeighborFunc = pFunctionStart - (j * 0x20);
                                    if (reinterpret_cast<uintptr_t>(pNeighborFunc) < reinterpret_cast<uintptr_t>(module.m_pModuleBase)) break;
                                    if (*reinterpret_cast<uint32_t*>(pNeighborFunc) == 0xB8D18B4C)
                                    {
                                        uint32_t uNeighborSyscall = *reinterpret_cast<uint32_t*>(pNeighborFunc + 4);
                                        uSyscallNumber = uNeighborSyscall + j;
                                        break;
                                    }
                                }

                                // @note / SapDragon: search down
                                if (!uSyscallNumber)
                                {
                                    for (int j = 1; j < 20; ++j)
                                    {
                                        uint8_t* pNeighborFunc = pFunctionStart + (j * 0x20);
                                        if (reinterpret_cast<uintptr_t>(pNeighborFunc) > (reinterpret_cast<uintptr_t>(module.m_pModuleBase) + module.m_pNtHeaders->OptionalHeader.SizeOfImage)) break;
                                        if (*reinterpret_cast<uint32_t*>(pNeighborFunc) == 0xB8D18B4C)
                                        {
                                            uint32_t uNeighborSyscall = *reinterpret_cast<uint32_t*>(pNeighborFunc + 4);
                                            uSyscallNumber = uNeighborSyscall - j;
                                            break;
                                        }
                                    }
                                }
                            }
                        }


C++:
uint8_t* pNeighborFunc = pFunctionStart - (j * 0x20);

на старых осях (server2012 привет) шаг в 32 байта неактуален, стабы сервисов имеют длину как правило в 10 байт (против 20 если MajorVersion == 10), поэтому округляя по 16-байтной границе шаг должен составлять 0xA байт
непонятно только зачем добавили поиск по сигнатурам, метод с сопоставлением записей с директории исключений выглядит ультимативным и изобильным, думаю в современной молвари стоит отказаться от всех этих метод со сканом сигнатур,
учитывая что на хосте может происходить подобная бредятина (схендлит ли ваш сигсканнер вот ЭТО?):

photo_2025-06-22_00-51-37.jpg
 
на старых осях (server2012 привет) шаг в 32 байта неактуален, стабы сервисов имеют длину как правило в 10 байт (против 20 если MajorVersion == 10), поэтому округляя по 16-байтной границе шаг должен составлять 0xA байт
непонятно только зачем добавили поиск по сигнатурам, метод с сопоставлением записей с директории исключений выглядит ультимативным и изобильным, думаю в современной молвари стоит отказаться от всех этих метод со сканом сигнатур,
учитывая что на хосте может происходить подобная бредятина (схендлит ли ваш сигсканнер вот ЭТО?):
Вот только зачем все это надо, если можно просто сортировать по rva..
 
отличная либа, ничо не скажешь,

особенно понравился детект нопов которые (непонятно почему, хотелось бы узнать) иногда появляются по адресу экспортов на системах с установленными EDR

C++:
 private:
                static bool isFunctionHooked(const uint8_t* pFunctionStart)
                {
                    const uint8_t* pCurrent = pFunctionStart;

                    while (*pCurrent == 0x90)
                        pCurrent++;

                    switch (pCurrent[0])
                    {
                        // @note / SapDragon: JMP rel32
                    case 0xE9:
                        // @note / SapDragon: JMP rel8
                    case 0xEB:
                        // @note / SapDragon: push imm32
                    case 0x68:
                        return true;

                        // @note / SapDragon: jmp [mem] / jmp [rip + offset]
                    case 0xFF:
                        if (pCurrent[1] == 0x25)
                            return true;
                        break;

                        // @note / SapDragon: int3...
                    case 0xCC:
                        return true;

                        // @note / SapDragon: ud2
                    case 0x0F:
                        if (pCurrent[1] == 0x0B)
                            return true;
                        break;

                        // @note / SapDragon: int 0x3
                    case 0xCD:
                        if (pCurrent[1] == 0x03)
                            return true;
                        break;

                    default:
                        break;
                    }

                    return false;
                }
            };
        } // parsing
    } // policies

а вот тут вы и попалися.

C++:
if constexpr (platform::isWindows64)
                        {
                            if (isFunctionHooked(pFunctionStart) && !uSyscallNumber)
                            {
                                // @note / SapDragon: stable only on x64

                                // @note / SapDragon: search up
                                for (int j = 1; j < 20; ++j)
                                {
                                    uint8_t* pNeighborFunc = pFunctionStart - (j * 0x20);
                                    if (reinterpret_cast<uintptr_t>(pNeighborFunc) < reinterpret_cast<uintptr_t>(module.m_pModuleBase)) break;
                                    if (*reinterpret_cast<uint32_t*>(pNeighborFunc) == 0xB8D18B4C)
                                    {
                                        uint32_t uNeighborSyscall = *reinterpret_cast<uint32_t*>(pNeighborFunc + 4);
                                        uSyscallNumber = uNeighborSyscall + j;
                                        break;
                                    }
                                }

                                // @note / SapDragon: search down
                                if (!uSyscallNumber)
                                {
                                    for (int j = 1; j < 20; ++j)
                                    {
                                        uint8_t* pNeighborFunc = pFunctionStart + (j * 0x20);
                                        if (reinterpret_cast<uintptr_t>(pNeighborFunc) > (reinterpret_cast<uintptr_t>(module.m_pModuleBase) + module.m_pNtHeaders->OptionalHeader.SizeOfImage)) break;
                                        if (*reinterpret_cast<uint32_t*>(pNeighborFunc) == 0xB8D18B4C)
                                        {
                                            uint32_t uNeighborSyscall = *reinterpret_cast<uint32_t*>(pNeighborFunc + 4);
                                            uSyscallNumber = uNeighborSyscall - j;
                                            break;
                                        }
                                    }
                                }
                            }
                        }


C++:
uint8_t* pNeighborFunc = pFunctionStart - (j * 0x20);

на старых осях (server2012 привет) шаг в 32 байта неактуален, стабы сервисов имеют длину как правило в 10 байт (против 20 если MajorVersion == 10), поэтому округляя по 16-байтной границе шаг должен составлять 0xA байт
непонятно только зачем добавили поиск по сигнатурам, метод с сопоставлением записей с директории исключений выглядит ультимативным и изобильным, думаю в современной молвари стоит отказаться от всех этих метод со сканом сигнатур,
учитывая что на хосте может происходить подобная бредятина (схендлит ли ваш сигсканнер вот ЭТО?):

Посмотреть вложение 108988
хорошо, спасибо, поправлю, нопы например сцилла оставляет. Да и этот метод существует буквально как фаллбек, думаю ситуации что исключения на х64, и сортировка х86 подведут очень маловероятны
 
Скрытое содержимое
Не полезут, смысла от этого 0 им
Всю логику они этим поломают

Ладно заменят rva в табличке - сработает только на х32, на х64 нужна установка трамплина (так как рва в таблице экспорта подразумевает 4 байтовое число, а адреса в х64 8 байтовые) - а для этого искать память в нтдлл для его установки

каждый трамплин это байт 10-15

Считай сколько памяти на это нужно будет))
А выделить память рядом с нтдлл не всегда возможно - так что адекватной порчи таблицы экспорта не будет

У меня на виртуалке сискол парсер насчитал 463 Nt функции, также не забываем что табличка экспортит Zw - указывающие на тот же рва

463 * на минимальне 10 байт (или сколько там Mov rax, jmp rax)
получаем 4.5 кб - минимума для установка хуков на все нт функции
 


Напишите ответ...
  • Вставить:
Прикрепить файлы
Верх