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

Receiver

leet-cheats 👑
Автор темы
Модератор
661
997
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

Новичок
5
11

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

annihilatorq

Новичок
5
11
всем привет, открыл good first issue - https://github.com/annihilatorq/omni/issues/39. если у кого-то есть желание законтрибьютить в проект/опенсурс свой первый pull request и реализовать функционал C++29 используя С++26, то это отличная входная точка.

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

пример реализации: https://github.com/stephenberry/glaze/commit/e156acc9581e8b5ab3ad44d3151d6e5d1bd1620c
 
  • Нравится
Реакции: Receiver и nullptr-

annihilatorq

Новичок
5
11
всем привет, добавили возможность итерироваться по системным процессам и kernel-modules в стиле forward-range. класс, как и все другие ренжи в библиотеке, совместим с std::ranges/std::views.

API специально следует тому же простому и устоявшемуся стилю библиотеки - omni::things является итерируемым по omni::thing.

[+] — Добавлен .lookup() метод возвращающий std::optional<T>, принятый в С++29 для всех типов map в STL. Для ренжей из omni его можно использовать с C++23, в силу специфики наших итераторов (нам не нужен optional-ref из C++26):
C++:
const auto kernel32_exports = omni::get_module("kernel32.dll").named_exports();

// Chain of monadic operations
const auto process_id_address = kernel32_exports.lookup("GetCurrentProcessId").transform(&omni::named_export::address);

// Regular std::optional::has_value() check
const auto sleep_export = kernel32_exports.lookup("Sleep");
if (sleep_export) {
  std::println("Sleep @ {}", sleep_export->address);
}


[+] — Добавлены omni::processes, omni::process, omni::process_access:
examples/process.cpp:
namespace {

  [[nodiscard]] std::string to_utf8(std::wstring_view wide) {
    if (wide.empty()) {
      return {};
    }
    const int size =
      ::WideCharToMultiByte(CP_UTF8, 0, wide.data(), static_cast<int>(wide.size()), nullptr, 0, nullptr, nullptr);
    std::string narrow(static_cast<std::size_t>(size), '\0');
    ::WideCharToMultiByte(CP_UTF8, 0, wide.data(), static_cast<int>(wide.size()), narrow.data(), size, nullptr, nullptr);
    return narrow;
  }

  [[nodiscard]] std::string process_name(const omni::process& process) {
    return process.name().empty() ? "<unnamed>" : to_utf8(process.name());
  }

  [[nodiscard]] auto by_id(std::uint32_t id) {
    return [id](const omni::process& process) {
      return process.id() == id;
    };
  }

} // namespace

int main() {
  ::SetConsoleOutputCP(CP_UTF8);

  auto snapshot = omni::processes::snapshot();
  if (!snapshot) {
    std::println("Failed to enumerate processes: {}", snapshot.error().message());
    return 1;
  }

  std::println("First 10 entries of the snapshot:");
  for (const omni::process process : *snapshot | std::views::take(10)) {
    std::println("  pid {:>6}  parent {:>6}  threads {:>3}  {}",
      process.id(),
      process.parent_id(),
      process.thread_count(),
      process_name(process));
  }

  auto current_process = std::ranges::find_if(*snapshot, by_id(::GetCurrentProcessId()));
  if (current_process == snapshot->end()) {
    std::println("Current process was not found.");
    return 1;
  }

  // omni::process is a view type that is cheap to copy
  const omni::process self = *current_process;

  const auto parent_it = std::ranges::find_if(*snapshot, by_id(self.parent_id()));
  std::println();
  std::println("Current process: {} (pid {})", process_name(self), self.id());
  std::println("Parent:          {}", parent_it == snapshot->end() ? std::string{"<exited>"} : process_name(*parent_it));

  auto handle = self.open_handle();
  if (!handle) {
    std::println("open_handle failed: {}", handle.error().message());
    return 1;
  }
  std::println("Opened own handle: {}", ::GetProcessId(handle->get()) == self.id());

  const auto system_it = std::ranges::find_if(*snapshot, by_id(4));
  if (system_it != snapshot->end()) {
    const auto system_handle = system_it->open_handle(omni::process_access::all_access);
    if (!system_handle && system_handle.error() == std::errc::permission_denied) {
      std::println("System (pid 4) with all_access: permission denied, as expected without SeDebugPrivilege");
    }
  }
}


[+] — Добавлены omni::kernel_modules, omni::kernel_module.
examples/kernel_modules.cpp:
int main() {
  auto snapshot = omni::kernel_modules::snapshot();
  if (!snapshot) {
    std::println("Failed to enumerate kernel modules: {}", snapshot.error().message());
    return 1;
  }

  std::println("A kernel module snapshot becomes a normal forward range ({} modules):", snapshot->size());
  for (const auto& module : *snapshot | std::views::take(10)) {
    std::println("  base={:p} size={:#010x} name={}", module.base_address().ptr(), module.size(), module.name());
  }

  auto kernel =
    std::ranges::find_if(*snapshot, [](const omni::kernel_module& module) { return module.load_order_index() == 0; });
  if (kernel == snapshot->end()) {
    std::println("Kernel image was not found.");
    return 1;
  }

  std::println();
  std::println("The first module in load order is the kernel image:");
  std::println("  path : {}", kernel->path());
  std::println("  name : {}", kernel->name());
  std::println("  base : {:p}", kernel->base_address().ptr());
}




спасибо @Receiver и cluedesc
 
  • Нравится
Реакции: Receiver