Подписывайтесь на наш Telegram и не пропускайте важные новости! Перейти

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

Stop Staring At the Shadows
Участник
Участник
Статус
Оффлайн
Регистрация
10 Окт 2020
Сообщения
537
Реакции
531
1777197237337.png

Репозиторий:
Пожалуйста, авторизуйтесь для просмотра ссылки.

Документация:
Пожалуйста, авторизуйтесь для просмотра ссылки.


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

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

Новое C++23 API для экспортов:
Omni module:
Expand Collapse Copy
#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("  named exports        : {}", self.named_exports().size());
  std::println("  ordinal exports      : {}", self.ordinal_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.named_exports();
  auto first_named_exports = kernel32_exports | 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. Добавил несколько видов енумерации экспортов для максимально подходящего вам варианта: .named_exports(), .ordinal_exports().
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::named_exports итерируется по omni::named_export
- omni::ordinal_exports итерируется по omni::ordinal_export
- omni::api_sets итерируется по omni::api_set
- omni::api_set_hosts итерируется по omni::api_set_host
- omni::syscaller, функция omni::syscall
- omni::inline_syscaller, функция omni::inline_syscall
- omni::lazy_importer, функция omni::lazy_import

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

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

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

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

Новое C++23 API для экспортов:
Omni module:
Expand Collapse Copy
#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-файлы библиотеки в один:
Пожалуйста, авторизуйтесь для просмотра ссылки.

Спасибо всем тем кто использует библиотеку и ставит звёзды. Если у вас появятся какие-либо проблемы при её использовании, создавайте Issue на гитхаб. Буду рад каждому контрибьютору.
Как библиотека ведёт себя с manually mapped DLL? Они видны через PEB и работает ли на них поиск экспортов и ленивый импорт?
 
Как библиотека ведёт себя с manually mapped DLL? Они видны через PEB и работает ли на них поиск экспортов и ленивый импорт?
привет, библиотека итерируется по списку PEB->loader_data->in_load_order_module_list и по сути отдаёт пользователю указатель на loader_table_entry с некоторым сахаром в виде пары методов, соответственно если после ммапинга модуль не регистрируется в PEB->Ldr->InLoadOrderModuleList (а это afaik делают далеко не все, тот же Blackbone это делает через CreateNTReference), то omni в списке модулей его не увидит. по поводу EAT ситуация уже другая. если export_dir и хедеры не затирались, то зная base, можно сделать omni::named_exports{base_address} и библиотека спарсит адрес export-директории самостоятельно (предполагая что с таблицей ординалов/адресов всё будет нормально)
по поводу ленивого импорта - нет, не выйдет, это эдж-кейс который я особо не предусматривал при разработке, адрес импорта ищется линейно (поиск становится в разы быстрее если вызывать перегрузку с именем модуля): перебираем все модули -> нашли модуль -> перебираем все named экспорты -> нашли экспорт -> вызвали с переданными аргументами. в целом lazy_importer это сахар над вполне простой и очевидной операцией, ты легко можешь сделать что-то наподобии:

C++:
Expand Collapse Copy
int main() {
  omni::address mapped_module_base{1000};
  omni::named_exports mapped_module_eat{mapped_module_base};

  auto module_export = mapped_module_eat.find("MessageBoxA");
  if (module_export == mapped_module_eat.end()) {
    return 1;
  }

  // Optional, because omni::address::invoke assumes that address can be nullptr
  std::optional<int> result = module_export->address.invoke<int>(nullptr, "String 1", "String 2", MB_OK);
}
 
Последнее редактирование:
Добавили ограниченную поддержку inline_syscall (right now only at Clang/GCC), а так же syscaller user-policy через NTTP, либо runtime.

Disassembly inline_syscall:
1777723842051.png


syscaller user-policy:
C++:
Expand Collapse Copy
// NTTP Simple (compile-time)
omni::inline_syscall("NtCreateThreadEx", ...);
// Встроит syscall в месте вызова

// NTTP Detailed (compile-time)
omni::syscaller<omni::status, omni::syscaller_options<
  default_syscall_id_parser,
  inline_syscall_invoker>> syscaller{"NtCreateThreadEx"};
// Встроит syscall в месте вызова

// ARGS Detailed (Runtime)
omni::syscaller<omni::status> syscaller{"NtCreateThreadEx",
  omni::syscaller_options{.parser = ..., .invoker = ...}};
// Встроит syscall, но через один jmp
 

1779056161779.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:
Expand Collapse Copy
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:
Expand Collapse Copy
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...
}
 
Назад
Сверху Снизу