Исходник Omni. C++23 library for low-level user-mode Windows programming

Receiver

leet-cheats 👑
Автор темы
Модератор
662
1,005
1777197237337.png

Репозиторий: https://github.com/annihilatorq/omni
Документация: https://annihilatorq.github.io/omni/en/docs/

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

Всё было имплементировано в PR #14, после опыта написания пары библиотек автор считает что спроектировал новый API не идеально, но в разы лучше предыдущего.

Новое C++23 API для экспортов:
C++:
#include <Windows.h>

#include "omni/module.hpp"
#include "omni/modules.hpp"

#include <print>
#include <ranges>
#include <string_view>

[[nodiscard]] std::string_view name_of_export(const omni::module_export& export_entry) {
  return export_entry.name;
}

int main() {
  auto self = omni::base_module();
  auto kernel32 = omni::get_module(L"kernel32.dll");

  auto* optional_header = self.image()->get_optional_header();

  std::println("Current image:");
  std::println("  name                 : {}", self.name());
  std::println("  path                 : {}", self.system_path().string());
  std::println("  base                 : {:#x}", self.base_address().value());
  std::println("  entry point          : {:#x}", self.entry_point().value());
  std::println("  size of image        : {}", optional_header->size_image);
  std::println("  export count         : {}", self.exports().size());

  std::println();
  std::println("Kernel32 convenience helpers:");
  std::println("  name                 : {}", kernel32.name());
  std::println("  path                 : {}", kernel32.system_path().string());
  std::println(R"(  matches "kernel32"   : {})", kernel32.matches_name_hash(L"kernel32"));
  std::println(R"(  matches "KERNEL32.DLL": {})", kernel32.matches_name_hash(L"KERNEL32.DLL"));

  std::println();
  std::println("First five named exports from kernel32:");

  auto kernel32_exports = kernel32.exports();
  auto first_named_exports = kernel32_exports.named() | std::views::transform(name_of_export) | std::views::take(5);
  for (std::string_view export_name : first_named_exports) {
    std::println("  {}", export_name);
  }
}

Главные функциональные изменения:
1. Добавлена поддержка кастомных compile-time хэшей, они обязаны соответствовать описанному концепту в библиотеке. Пример с использованием кастомного хэша можно найти в репозитории.
2. Имплементирована логика резольвинга ApiSetMap при поиске экспорта. Теперь, если forwarded-экспорт указывает на DLL являющуюся ApiSetом, библиотека автоматически попытается найти хост закрепленный за ApiSetом. Если хост закрепленный за ApiSetом загружен в текущий процесс, то библиотека найдёт его в списке модулей и попытается найти указанную функцию в EAT этого самого модуля.
3. Добавлен omni::status с методами для упрощения работы с NTSTATUS. В структуре лежит POD std::int32_t из-за чего ABI будет воспринимать этот тип как INTEGER, не передавая hidden-function-pointer в RCX. По этой причине тип можно безопасно использовать для возвращаемых значений Nt/Zw функций
4. Добавил несколько видов енумерации экспортов для максимально подходящего вам варианта: .all(), .named(), .ordinal(). Итерация по всем (.all()) экспортам зачастую будет самой долгой из-за устройства EAT и сортировки имён в нём, из-за чего при итерации по num_functions код вынужден произвести O(n) поиск имени в списке num_names.
5. Улучшено удобство класса omni::address, убран суффикс _t

Фиксы:
1. Пофикшена неверная итерация по ordinal-экспортам. В shadow_syscall использовалось num_names вместо num_functions)
2. Пофикшена проблема с кэшированием экспорта в omni::lazy_importer по одному имени, теперь кэшируется module_name_hash и export_name_hash. В shadow_syscall экспорт кэшировался по хэшу имени функции, из-за чего появлялись проблемы если в нескольких загруженных модулях присутствовал экспорт с одинаковым именем.
3. Пофикшена проблема с кэшированием экспорта в omni::lazy_importer без проверки на то, был ли перезагружен модуль в процесс. В shadow_syscall игнорировалась ситуация когда DLL модуль внутри которого находился нужный экспорт был выгружен из процесса и загружен снова уже на другом base_address. Теперь это учитывается.
В целом есть ещё очень много минорных фиксов о которых писать нет смысла, библиотека в целом стала гораздо более надежной.

Концептуальные изменения:
1. Zero-allocation design, при обычном использовании библиотеки не будет использовано никаких выделений памяти. Единственные микро-аллокации которые могут произойти, это аллокации при использовании .to_path() (std::filesystem::path аллоцирует строку) и .string() (для конвертации между std::wstring_view в std::string) методов из win::unicode_string. Это методы-утилиты, внутри себя библиотека их не использует. Если не вызывать эти методы - весь код который вы пишете будет работать как view по уже существующей памяти.
2. Новое предсказуемое и четкое разделение API:
- omni::modules итерируется по omni::module.
- omni::module_exports итерируется по omni::module_export
- omni::api_sets итерируется по omni::api_set
- omni::api_set_hosts итерируется по omni::api_set_host
- omni::syscaller, функция omni::syscall
- omni::lazy_importer, функция omni::lazy_import

Тесты:
Библиотека теперь имеет хороший набор юнит-тестов и большую CI матрицу из 16 джобов (на момент 26.04.2026), компилируется и тестируется программа на таких компиляторах как: MSVC, Clang-CL, GCC, на архитектурах x64 & x86, Debug & Release сборках. Библиотека так же может быть собрана без исключений.

Для тех кому трудно или для тех кто не имеет желания работать с CMake есть CI воркфлоу который мёржит все header-файлы библиотеки в один:

Эта тема опубликована на правах repository maintainer. Спасибо всем тем кто использует библиотеку и ставит звёзды. Если у вас появятся какие-либо проблемы при её использовании, создавайте Issue на гитхаб. Будем рады каждому контрибьютору.
 
Последнее редактирование:

annihilatorq

Новичок
2
9

1779055980386.png


[+] — Добавлен omni::unique_handle, RAII обёртка над сырым HANDLE. По дизайну, целям и философии класс полностью идентичен std::unique_ptr с парой доп. методов для удобства.

omni::unique_ptr при уничтожении (~dtor, .reset(), .close()) на x64 архитектуре сисколлит NtClose (для clang/gcc через inline_syscall, для MSVC через syscaller), на x86 просто вызывает адрес экспорта NtClose.

Пользы у класса +- столько же, сколько и от перехода с new/delete на умные указатели для управления памятью (при корректном использовании убирает возможность ликнуть какой-либо хендл).

Вот пример функции с сырыми HANDLE и ручным управлением:
Raw HANDLE example:
void raw_handle_example() {
  HANDLE read_pipe{};
  HANDLE write_pipe{};

  if (::CreatePipe(&read_pipe, &write_pipe, nullptr, 0U) == FALSE) {
    return;
  }

  constexpr char payload[] = "omni";
  constexpr auto payload_size = static_cast<DWORD>(sizeof(payload));
  DWORD bytes_written{};

  if (::WriteFile(write_pipe, payload, payload_size, &bytes_written, nullptr) == FALSE || bytes_written != payload_size) {
    ::CloseHandle(read_pipe);
    ::CloseHandle(write_pipe);
    return;
  }

  char buffer[sizeof(payload)]{};
  DWORD bytes_read{};

  if (::ReadFile(read_pipe, buffer, payload_size, &bytes_read, nullptr) == FALSE || bytes_read != payload_size) {
    ::CloseHandle(read_pipe);
    ::CloseHandle(write_pipe);
    return;
  }

  // do something...

  ::CloseHandle(read_pipe);
  ::CloseHandle(write_pipe);
}

А вот пример точно той же функции, но с использованием omni::unique_handle

RAII omni::unique_handle example:
void raii_handle_example() {
  omni::unique_handle read_pipe;
  omni::unique_handle write_pipe;

  if (::CreatePipe(read_pipe.out_ptr(), write_pipe.out_ptr(), nullptr, 0U) == FALSE) {
    return;
  }

  constexpr char payload[] = "omni";
  constexpr auto payload_size = static_cast<DWORD>(sizeof(payload));
  DWORD bytes_written{};

  if (::WriteFile(write_pipe.get(), payload, payload_size, &bytes_written, nullptr) == FALSE ||
      bytes_written != payload_size) {
    return;
  }

  char buffer[sizeof(payload)]{};
  DWORD bytes_read{};

  if (::ReadFile(read_pipe.get(), buffer, payload_size, &bytes_read, nullptr) == FALSE || bytes_read != payload_size) {
    return;
  }

  // do something...
}
 
Последнее редактирование модератором:
  • Нравится
Реакции: Receiver