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

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

Stop Staring At the Shadows
Участник
Участник
Статус
Оффлайн
Регистрация
10 Окт 2020
Сообщения
535
Реакции
521
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("  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 на гитхаб. Буду рад каждому контрибьютору.
 
Последнее редактирование:
Посмотреть вложение 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::module_exports{base_address} и библиотека спарсит адрес export-директории самостоятельно (предполагая что с таблицей ординалов/адресов всё будет нормально)
по поводу ленивого импорта - нет, не выйдет, это эдж-кейс который я особо не предусматривал при разработке, адрес импорта ищется линейно (поиск становится в разы быстрее если вызывать перегрузку с именем модуля): перебираем все модули -> нашли модуль -> перебираем все named экспорты -> нашли экспорт -> вызвали с переданными аргументами. в целом lazy_importer это сахар над вполне простой и очевидной операцией, ты легко можешь сделать что-то наподобии:

C++:
Expand Collapse Copy
int main() {
  omni::address mapped_module_base{1000};
  omni::module_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);
}
 
Назад
Сверху Снизу