Хотели ли Вы когда-нибудь заглянуть под капот операционной системы, посмотреть на внутреннее устройство её механизмов, покрутить винтики и посмотреть на открывшиеся возможности? Возможно, даже хотели поработать напрямую с железом, но считали, что драйвера — rocketscience?
Предлагаю вместе пройтись по мостику в ядро и посмотреть, насколько глубока кроличья нора.
Итак, представляю драйвер-фреймворк для kernel-хакинга, написанный на C++17, и призванный, по возможности, снять барьеры между ядром и юзермодом или максимально сгладить их присутствие. А также, набор юзермодных и ядерных API и обёрток для быстрой и удобной разработки в Ring0 как для новичков, так и для продвинутых программистов.
Основные возможности:
… и многое другое.
А начнём мы с загрузки и подключения фреймворка в наш проект на C++.
GitHub
Для сборки очень желательно пользоваться новейшей версией Visual Studio и последним доступным пакетом WDK (Windows Driver Kit), скачать который можно с официального сайта Microsoft.
Для тестирования прекрасно подойдёт бесплатный VMware Player с установленной Windows, не ниже Windows 7, любой разрядности.
Сборка тривиальная и вопросов не вызовет:
В результате мы получим драйвер, юзермодную библиотеку, а также сопутствующие служебные файлы (*.inf для ручной установки, *.cab для подписи драйвера на Microsoft Hardware Certification Publisher и т.д.).
Для установки драйвера (если нет необходимой для х64 цифровой подписи кода — соответствующего EV-сертификата) нужно перевести систему в тестовый режим, игнорирующий наличие цифровой подписи у драйверов. Для этого выполним в командной строке от имени администратора:
… и перезагрузим машину. Если всё сделано правильно, в нижнем правом углу появится надпись, что Windows находится в тестовом режиме.
Настройка тестовой среды завершена, приступим к использованию API в нашем проекте.
Фреймворк имеет следующую иерархию:
/Kernel-Bridge/API — набор функций для использования в драйверах и ядерных модулях, не имеют внешних зависимостей и могут быть свободно использованы в сторонних проектах
/User-Bridge/API — набор юзермодных обёрток над драйвером и служебные функции для работы с PE-файлами, PDB-символами и т.д.
/SharedTypes/ — одновременно и юзермодные, и ядерные хедеры, содержащие необходимые общие типы
Драйвер можно загрузить двумя способами: как обычный драйвер и как минифильтр. Второй способ предпочтительный, т.к. открывает доступ к расширенному функционалу фильтров и юзермодных каллбэков на системные события.
Итак, создадим консольный проект на C++, подключим необходимые заголовочные файлы и загрузим драйвер:
Отлично! Теперь мы можем использовать API и взаимодействовать с ядром.
Начнём с наиболее востребованного функционала в среде разработчиков читов — чтение и запись памяти чужого процесса:
Ничего сложного! Опустимся на уровень ниже — чтение и запись ядерной памяти:
А что насчёт функций для взаимодействия с железом? Например, I/O-порты.
Пробросим их в юзермод, взведя 2 бита IOPL в регистре EFlags, отвечающие за уровень привилегий, на котором доступны инструкции in/out/cli/sti.
Таким образом, мы сможем выполнять их в юзермоде без ошибки Privileged Instruction:
Но что насчёт настоящей свободы? Ведь зачастую хочется выполнить произвольный код с привилегиями ядра. Напишем весь ядерный код в юзермоде и передадим на него управление из ядра (SMEP отключается автоматически, перед вызовом драйвер сохраняет FPU-контекст и сам вызов происходит внутри try..except-блока):
Но кроме баловства с шеллами, есть и серьёзный функционал, позволяющий создавать простейшие DLP на основе подсистемы файловых, объектных и процессных фильтров.
Фреймворк позволяет фильтровать CreateFile/ReadFile/WriteFile/DeviceIoControl, а также события открытия\дуплицирования хэндлов (ObRegisterCallbacks) и события запуска процессов\потоков и подгрузки модулей (PsSet***NotifyRoutine). Это позволит, к примеру, блокировать доступ к произвольным файлам или подменять информацию о серийных номерах жёсткого диска.
Принцип работы:
Пример подписки на ObRegisterCallbacks и урезание доступа к текущему процессу:
Итак, мы вкратце пробежались по основным моментам юзермодной части фреймворка, но за кадром остался ядерный API.
Весь API и обёртки расположены в соответствующей папке: /Kernel-Bridge/API/
Они включают работу с памятью, с процессами, со строками и блокировками, и много с чем ещё, существенно упрощая разработку своих собственных драйверов. API и обёртки зависят только от самих себя и не зависят от внешнего окружения: Вы можете свободно использовать их в своём собственном драйвере.
Пример работы со строками в ядре — камень преткновения всех новичков:
Если же Вы хотите реализовать свой собственный обработчик для своего IOCTL-кода, вы очень легко можете сделать это по следующей схеме:
Поддерживаются все три вида ввода-вывода (METHOD_BUFFERED, METHOD_NEITHER и METHOD_IN_DIRECT/METHOD_OUT_DIRECT), по-умолчанию используется METHOD_NEITHER.
Вот и всё! В статье охвачена лишь малая толика всех возможностей. Надеюсь, фреймворк будет полезен начинающим разработчикам компонентов ядра, реверс-инженерам, разработчикам читов, античитов и защит.
А также, принять участие в разработке приглашаются все желающие. В дальнейших планах:
Благодарю за внимание!
Предлагаю вместе пройтись по мостику в ядро и посмотреть, насколько глубока кроличья нора.
Итак, представляю драйвер-фреймворк для kernel-хакинга, написанный на C++17, и призванный, по возможности, снять барьеры между ядром и юзермодом или максимально сгладить их присутствие. А также, набор юзермодных и ядерных API и обёрток для быстрой и удобной разработки в Ring0 как для новичков, так и для продвинутых программистов.
Основные возможности:
- Доступ к портам ввода-вывода, а также проброс инструкций in, out, cli и sti в юзермод через IOPL
- Обёртки над системной пищалкой
- Доступ к MSR (Model-Specific Registers)
- Набор функций для доступа к юзермодной памяти других процессов и к памяти ядра
- Работа с физической памятью, DMI/SMBIOS
- Создание юзермодных и ядерных потоков, доставка APC
- Юзермодные Ob*** и Ps***-каллбэки и фильтры файловой системы
- Загрузка неподписанных драйверов и ядерных библиотек
… и многое другое.
А начнём мы с загрузки и подключения фреймворка в наш проект на C++.
GitHub
Для сборки очень желательно пользоваться новейшей версией Visual Studio и последним доступным пакетом WDK (Windows Driver Kit), скачать который можно с официального сайта Microsoft.
Для тестирования прекрасно подойдёт бесплатный VMware Player с установленной Windows, не ниже Windows 7, любой разрядности.
Сборка тривиальная и вопросов не вызовет:
- Открываем Kernel-Bridge.sln
- Выбираем нужную разрядность
- Ctrl+Shift+B
В результате мы получим драйвер, юзермодную библиотеку, а также сопутствующие служебные файлы (*.inf для ручной установки, *.cab для подписи драйвера на Microsoft Hardware Certification Publisher и т.д.).
Для установки драйвера (если нет необходимой для х64 цифровой подписи кода — соответствующего EV-сертификата) нужно перевести систему в тестовый режим, игнорирующий наличие цифровой подписи у драйверов. Для этого выполним в командной строке от имени администратора:
bcdedit.exe /set loadoptions DISABLE_INTEGRITY_CHECKS
bcdedit.exe /set TESTSIGNING ON
… и перезагрузим машину. Если всё сделано правильно, в нижнем правом углу появится надпись, что Windows находится в тестовом режиме.
Настройка тестовой среды завершена, приступим к использованию API в нашем проекте.
Фреймворк имеет следующую иерархию:
/Kernel-Bridge/API — набор функций для использования в драйверах и ядерных модулях, не имеют внешних зависимостей и могут быть свободно использованы в сторонних проектах
/User-Bridge/API — набор юзермодных обёрток над драйвером и служебные функции для работы с PE-файлами, PDB-символами и т.д.
/SharedTypes/ — одновременно и юзермодные, и ядерные хедеры, содержащие необходимые общие типы
Драйвер можно загрузить двумя способами: как обычный драйвер и как минифильтр. Второй способ предпочтительный, т.к. открывает доступ к расширенному функционалу фильтров и юзермодных каллбэков на системные события.
Итак, создадим консольный проект на C++, подключим необходимые заголовочные файлы и загрузим драйвер:
#include <Windows.h> #include "WdkTypes.h" // Универсальные типы для x32/x64 и определения из WDK #include "CtlTypes.h" // Определения структур IOCTL-запросов для драйвера #include "User-Bridge.h" // API, предоставляемый драйвером int main() { using namespace KbLoader; BOOL Status = KbLoadAsFilter( L"X:\\Folder\\Path\\To\\Kernel-Bridge.sys", L"260000" // Высота нашего драйвера в стеке фильтров ); if (!Status) return 0; // Драйвер успешно загружен! // Теперь мы можем использовать API ... // Выгружаемся: KbUnload(); return 0; }
Отлично! Теперь мы можем использовать API и взаимодействовать с ядром.
Начнём с наиболее востребованного функционала в среде разработчиков читов — чтение и запись памяти чужого процесса:
using namespace Processes::MemoryManagement; constexpr int Size = 64; BYTE Buffer[Size] = {}; BOOL Status = KbReadProcessMemory( // Или KbWriteProcessMemory, чтобы записать ProcessId, 0x7FFF0000, // Желаемый адрес в контексте ProcessId &Buffer, Size );
Ничего сложного! Опустимся на уровень ниже — чтение и запись ядерной памяти:
using namespace VirtualMemory; constexpr int Size = 64; BYTE Buffer[Size]; // И "куда", и "откуда" могут быть и ядерными, // и юзермодными адресами в контексте ТЕКУЩЕГО процесса: BOOL Status = KbCopyMoveMemory( reinterpret_cast<WdkTypes::PVOID>(Buffer), // Куда 0xFFFFF80000C00000, // Откуда Size, FALSE // Говорим драйверу, что буферы не пересекаются );
А что насчёт функций для взаимодействия с железом? Например, I/O-порты.
Пробросим их в юзермод, взведя 2 бита IOPL в регистре EFlags, отвечающие за уровень привилегий, на котором доступны инструкции in/out/cli/sti.
Таким образом, мы сможем выполнять их в юзермоде без ошибки Privileged Instruction:
#include <intrin.h> using namespace IO::Iopl; // Для примера, включим системную пищалку! KbRaiseIopl(); // Теперь in/out/cli/sti доступны в юзермоде! ULONG Frequency = 1000; // 1 kHz ULONG Divider = 1193182 / Frequency; __outbyte(0x43, 0xB6); // Установим режим прямоугольных импульсов // Устанавливаем делитель для частоты тактового генератора: __outbyte(0x42, static_cast<unsigned char>(Divider)); __outbyte(0x42, static_cast<unsigned char>(Divider >> 8)); __outbyte(0x61, __inbyte(0x61) | 3); // Включаем пищалку (подачу импульсов на мембрану) for (int i = 0; i < 5000; i++); // Можно попробовать Sleep(), но IOPL может сброситься при возврате в юзермод! __outbyte(0x61, __inbyte(0x61) & 252); // Останавливаем пищалку KbResetIopl();
Но что насчёт настоящей свободы? Ведь зачастую хочется выполнить произвольный код с привилегиями ядра. Напишем весь ядерный код в юзермоде и передадим на него управление из ядра (SMEP отключается автоматически, перед вызовом драйвер сохраняет FPU-контекст и сам вызов происходит внутри try..except-блока):
using namespace KernelShells; // Для примера вызовем KeStallExecutionProcessor: ULONG Result = 1337; KbExecuteShellCode( []( _GetKernelProcAddress GetKernelProcAddress, PVOID Argument ) -> ULONG { // Этот код будет выполнен в Ring0 // Динамически импортируем нужные ядерные функции: using _KeStallExecutionProcessor = VOID(WINAPI*)(ULONG Microseconds); auto Stall = reinterpret_cast<_KeStallExecutionProcessor>( GetKernelProcAddress(L"KeStallExecutionProcessor") ); Stall(1000 * 1000); // Останавливаем процессор на одну секунду ULONG Value = *static_cast<PULONG>(Argument); return Value == 1337 ? 0x1EE7C0DE : 0; }, &Result, // Argument &Result // Result ); // Функция вернёт Result = 0x1EE7C0DE
Но кроме баловства с шеллами, есть и серьёзный функционал, позволяющий создавать простейшие DLP на основе подсистемы файловых, объектных и процессных фильтров.
Фреймворк позволяет фильтровать CreateFile/ReadFile/WriteFile/DeviceIoControl, а также события открытия\дуплицирования хэндлов (ObRegisterCallbacks) и события запуска процессов\потоков и подгрузки модулей (PsSet***NotifyRoutine). Это позволит, к примеру, блокировать доступ к произвольным файлам или подменять информацию о серийных номерах жёсткого диска.
Принцип работы:
- Драйвер регистрирует файловые фильтры и устанавливает Ob***/Ps***-каллбэки
- Драйвер открывает Communication-порт, к которому подключаются клиенты, желающие подписаться на то или иное событие
- Юзермодные приложения присоединяются к порту и получают от драйвера данные о произошедшем событии, выполняют фильтрацию (урезают хэндлы в правах, блокируют доступ к файлу и т.д.) и возвращают событие в ядро
- Драйвер применяет полученные изменения
Пример подписки на ObRegisterCallbacks и урезание доступа к текущему процессу:
#include <Windows.h> #include <fltUser.h> #include "CommPort.h" #include "WdkTypes.h" #include "FltTypes.h" #include "Flt-Bridge.h" ... // Слушатель событий ObRegisterCallbacks: CommPortListener<KB_FLT_OB_CALLBACK_INFO, KbObCallbacks> ObCallbacks; // Предотвратим открытие хэндла нашего процесса с правами PROCESS_VM_READ: Status = ObCallbacks.Subscribe([]( CommPort& Port, MessagePacket<KB_FLT_OB_CALLBACK_INFO>& Message ) -> VOID { auto Data = static_cast<PKB_FLT_OB_CALLBACK_INFO>(Message.GetData()); if (Data->Target.ProcessId == GetCurrentProcessId()) { Data->CreateResultAccess &= ~PROCESS_VM_READ; Data->DuplicateResultAccess &= ~PROCESS_VM_READ; } ReplyPacket<KB_FLT_OB_CALLBACK_INFO> Reply(Message, ERROR_SUCCESS, *Data); Port.Reply(Reply); // Отвечаем драйверу });
Итак, мы вкратце пробежались по основным моментам юзермодной части фреймворка, но за кадром остался ядерный API.
Весь API и обёртки расположены в соответствующей папке: /Kernel-Bridge/API/
Они включают работу с памятью, с процессами, со строками и блокировками, и много с чем ещё, существенно упрощая разработку своих собственных драйверов. API и обёртки зависят только от самих себя и не зависят от внешнего окружения: Вы можете свободно использовать их в своём собственном драйвере.
Пример работы со строками в ядре — камень преткновения всех новичков:
#include <wdm.h> #include <ntstrsafe.h> #include <stdarg.h> #include "StringsAPI.h" WideString wString = L"Some string"; AnsiString aString = wString.GetAnsi().GetLowerCase() + " and another string!"; if (aString.Matches("*another*")) DbgPrint("%s\r\n", aString.GetData());
Если же Вы хотите реализовать свой собственный обработчик для своего IOCTL-кода, вы очень легко можете сделать это по следующей схеме:
- Пишете обработчик в /Kernel-Bridge/Kernel-Bridge/IOCTLHandlers.cpp
- В этом же файле добавляете обработчик в конец массива Handlers в функции DispatchIOCTL
- Добавляете индекс запроса в перечисление Ctls::KbCtlIndices в CtlTypes.h в ТУ ЖЕ ПОЗИЦИЮ, что и в массиве Handlers в п.2
- Вызываете свой обработчик из юзермода, написав обёртку в User-Bridge.cpp, произведя вызов с помощью функции KbSendRequest
Поддерживаются все три вида ввода-вывода (METHOD_BUFFERED, METHOD_NEITHER и METHOD_IN_DIRECT/METHOD_OUT_DIRECT), по-умолчанию используется METHOD_NEITHER.
Вот и всё! В статье охвачена лишь малая толика всех возможностей. Надеюсь, фреймворк будет полезен начинающим разработчикам компонентов ядра, реверс-инженерам, разработчикам читов, античитов и защит.
А также, принять участие в разработке приглашаются все желающие. В дальнейших планах:
- Обёртки для прямых манипуляций с PTE-записями и проброс ядерной памяти в юзермод
- Инжекторы на основе уже существующих функций создания потоков и доставки APC
- GUI-платформа для живого реверс-инжиниринга и исследования ядра Windows
- Скриптовый движок для выполнения кусочков ядерного кода
- Поддержка SEH в динамически загружаемых модулях
- Прохождение HLK-тестов
Благодарю за внимание!
