D2
Администратор
- Регистрация
- 19 Фев 2025
- Сообщения
- 4,380
- Реакции
- 0
Проект делал в спешке, хотел успеть к новому году, но увы на несколько дней пришлось отложить разработку из-за проблем в техникуме.
И все же, С Новым Годом всех вас!
Github / Showcase
tl;dr
Написал простой буткит, цель была лучше понять как работает процесс загрузки системы и саму разработку UEFI.
И получился мне кажется первый буткит с поддержкой коммуникации через юзермод с открытым кодом.
Думаю если кто-то позже будет изучать и пытатся собрать что-то своё то ему будет от чего отталкиваться.
- Вес скомпилированного буткита 7 килобайт
- Написан на C++ и немного asm
- Коммуникация с юзермодом через хук функции в ядре (не рантайм сервисы)
- Обходит и не триггерит KPP (Kernel Patch Guard)
- Проект собирается за два клика через Visual Studio без костылей
Функции:
- Чтение/запись виртуальной памяти ядра
- Чтение/запись памяти процесса (MMU парсинг)
- Завершение процессов
- Повышение привилегий процессов до NT/SYSTEM (LPE)
(можно добавить почти любой функционал так как можно вызывать экспортируемые функции из ядра)
Спойлер: Минусы
Не использует вообще никаких структур, все оффсеты захардкожены, из-за этого поддерживает только Windows 10 22H2. (к каждому оффсету я добавил откуда он взят, на случай если кто-то захочет портировать его на другие версии)
Нету модулей по инфицированию, обхода secure boot и так далее, цель была понять саму работу, можно считать это за PoC если так удобнее.
Спойлер: Анализ, разбор и обьяснение работы
Анализ Буткита
Здесь мы видим графическое представление работы буткита на базовом уровне.
Это поможет лучше понять, что будет происходить дальше.
UefiMain
В
Во-первых, сохраняет оригинальный адрес функции
Во-вторых, создает событие
ExitBootServices Wrapper (asm)
В
Как только адрес возврата получен, выполнение передается функции
Именно поэтому мы не можем использовать событие
ExitBootServices Hook
В
Поскольку
Исполняемые образы всегда загружаются с начала страницы памяти, поэтому базовый адрес всегда будет делится на 0x1000.
Кроме того, все исполняемые образы имеют заголовок DOS в начале, начинающийся с определенного magic значения.
Имея эту информацию, мы можем идти назад по памяти, страница за страницей, считывая первые байты каждой страницы и проверять значение DOS magic чтобы найти базовый адрес.
Следующим шагом является определение адреса
Почему именно
В структуре
Для этого мы используем простой сканер по паттерну, чтобы найти адрес
SetVirtualAddressMap Event
Помните событие, созданное в
Цель этого события — преобразовать адрес нашего будущего хука из физического в виртуальный.
До этого момента система работает только с физической памятью без виртуального адресного пространства.
На следующем этапе я объясню детали.
OslArchTransferToKernel Hook
На этом этапе у нас есть адрес
Получив базовый адрес
Я выбрал функцию
Во-первых, мы хотим установить связь между пользовательским режимом и драйвером UEFI.
Для этого наша функция ядра должна вызываться как syscall из библиотеки пользовательского режима
Подсказка — часто функции, являющиеся syscall'ами ядра, начинаются с префикса Nt или Zw
Основная причина выбора именно
Что это значит?
Как вы, возможно, знаете, в Windows есть функция безопасности, называемая Kernel Patch Guard (KPP).
Ее задача — сканировать память ядра на наличие изменений и вызывать BSOD, если они обнаружены.
Мы обходим KPP, модифицируя ядро до его выполнения, чтобы Patch Guard сравнивал уже измененное ядро с тем, что в памяти.
Однако проблема возникает при использовании хука с "трамплином".
Когда хук установлен, функция сначала прыгает на хук, выполняет свою работу, восстанавливает измененные байты, вызывает оригинальную функцию, а затем снова применяет "трамплин".
С активным KPP мы не можем удалять хук, так как это приведет к изменению ядра во время выполнения и вызовет синий экран от KPP.
Поэтому нам нужно найти способ заменить функционал оригинальной функции, не вызывая ее напрямую.
Функция враппер, как
NtUnloadKey Hook
В этой функции мы проверяем, соответствует ли переданный параметр нашей структуре команды. Если нет, мы возвращаем выполнение в
Если это наша команда, выполнение передается в
Анализ пользовательского режима
Как мы помним, функция
Таким образом, usermode может взаимодействовать с нашим хуком
Юзермод простой, но выполняет свою задачу.
Чтение и запись памяти процесса
Из интересного, чтение памяти процесса реализованно с помощью ручного парсинга MMU уровней Page Table.
Мы получаем адрес
В итоге мы не используем
Я возможно позже запишу статью на счет того как именно работает перевод адреса с вирт в физ.
Спасибо за внимание! Надеюсь, вы узнали что-то новое
И все же, С Новым Годом всех вас!
Github / Showcase
tl;dr
Написал простой буткит, цель была лучше понять как работает процесс загрузки системы и саму разработку UEFI.
И получился мне кажется первый буткит с поддержкой коммуникации через юзермод с открытым кодом.
Думаю если кто-то позже будет изучать и пытатся собрать что-то своё то ему будет от чего отталкиваться.
- Вес скомпилированного буткита 7 килобайт
- Написан на C++ и немного asm
- Коммуникация с юзермодом через хук функции в ядре (не рантайм сервисы)
- Обходит и не триггерит KPP (Kernel Patch Guard)
- Проект собирается за два клика через Visual Studio без костылей
Функции:
- Чтение/запись виртуальной памяти ядра
- Чтение/запись памяти процесса (MMU парсинг)
- Завершение процессов
- Повышение привилегий процессов до NT/SYSTEM (LPE)
(можно добавить почти любой функционал так как можно вызывать экспортируемые функции из ядра)
Спойлер: Минусы
Не использует вообще никаких структур, все оффсеты захардкожены, из-за этого поддерживает только Windows 10 22H2. (к каждому оффсету я добавил откуда он взят, на случай если кто-то захочет портировать его на другие версии)
Нету модулей по инфицированию, обхода secure boot и так далее, цель была понять саму работу, можно считать это за PoC если так удобнее.
Спойлер: Анализ, разбор и обьяснение работы
Анализ Буткита
Здесь мы видим графическое представление работы буткита на базовом уровне.
Это поможет лучше понять, что будет происходить дальше.
UefiMain
В
UefiMain
буткит выполняет две основные задачи: Во-первых, сохраняет оригинальный адрес функции
ExitBootServices
, чтобы восстановить его позже, и устанавливает хук на ExitBootServices
, перенаправляя вызовы в ExitBootServicesWrapper
. Во-вторых, создает событие
SetVirtualAddressMap
, которое будет объяснено позже. ExitBootServices Wrapper (asm)
В
ExitBootServicesWrapper
цель — извлечь адрес возврата из регистра RSP
. Как только адрес возврата получен, выполнение передается функции
ExitBootServicesHook
. Именно поэтому мы не можем использовать событие
ExitBootServices
— внутри события невозможно получить адрес возврата. ExitBootServices Hook
В
ExitBootServicesHook
задача заключается в нахождении базового адреса winload.efi
. Поскольку
ExitBootServices
вызывается из winload.efi
, и у нас есть его адрес возврата, мы знаем, что он указывает на область внутри winload.efi
. Исполняемые образы всегда загружаются с начала страницы памяти, поэтому базовый адрес всегда будет делится на 0x1000.
Кроме того, все исполняемые образы имеют заголовок DOS в начале, начинающийся с определенного magic значения.
Имея эту информацию, мы можем идти назад по памяти, страница за страницей, считывая первые байты каждой страницы и проверять значение DOS magic чтобы найти базовый адрес.
Следующим шагом является определение адреса
OslArchTransferToKernel
. Почему именно
OslArchTransferToKernel
? Эта функция вызывается, когда winload.efi
завершает работу, и передает адрес LoaderBlock
. В структуре
LoaderBlock
содержится список LoadOrderListHead
, в котором находится адрес ntoskrnl.exe
. Для этого мы используем простой сканер по паттерну, чтобы найти адрес
OslArchTransferToKernel
, и устанавливаем на него хук. SetVirtualAddressMap Event
Помните событие, созданное в
UefiMain
? Сейчас настало его время. Цель этого события — преобразовать адрес нашего будущего хука из физического в виртуальный.
До этого момента система работает только с физической памятью без виртуального адресного пространства.
На следующем этапе я объясню детали.
OslArchTransferToKernel Hook
На этом этапе у нас есть адрес
LoaderBlock
, и мы обходим структуру LIST_ENTRY
, чтобы найти базовый адрес ntoskrnl.exe
. Получив базовый адрес
ntoskrnl.exe
, следующий шаг — выбрать функцию в ядре для установки хука. Я выбрал функцию
NtUnloadKey
по нескольким причинам. Во-первых, мы хотим установить связь между пользовательским режимом и драйвером UEFI.
Для этого наша функция ядра должна вызываться как syscall из библиотеки пользовательского режима
ntdll.dll
. Подсказка — часто функции, являющиеся syscall'ами ядра, начинаются с префикса Nt или Zw
Основная причина выбора именно
NtUnloadKey
в том, что она является оберткой для функции CmUnloadKey
. Что это значит?
Как вы, возможно, знаете, в Windows есть функция безопасности, называемая Kernel Patch Guard (KPP).
Ее задача — сканировать память ядра на наличие изменений и вызывать BSOD, если они обнаружены.
Мы обходим KPP, модифицируя ядро до его выполнения, чтобы Patch Guard сравнивал уже измененное ядро с тем, что в памяти.
Однако проблема возникает при использовании хука с "трамплином".
Когда хук установлен, функция сначала прыгает на хук, выполняет свою работу, восстанавливает измененные байты, вызывает оригинальную функцию, а затем снова применяет "трамплин".
С активным KPP мы не можем удалять хук, так как это приведет к изменению ядра во время выполнения и вызовет синий экран от KPP.
Поэтому нам нужно найти способ заменить функционал оригинальной функции, не вызывая ее напрямую.
Функция враппер, как
NtUnloadKey
, идеально подходит для этого, так как чтобы восстановить оригинальный функционал нам надо просто передать параметры в другую функцию ядра.NtUnloadKey Hook
В этой функции мы проверяем, соответствует ли переданный параметр нашей структуре команды. Если нет, мы возвращаем выполнение в
CmUnloadKey
, имитируя поведение оригинальной функции.Если это наша команда, выполнение передается в
dispatcher
. (обработчик комманд)Анализ пользовательского режима
Как мы помним, функция
NtUnloadKey
из ntdll.dll
является syscall'ом в NtUnloadKey
находящийся в ntoskrnl.exe
. Таким образом, usermode может взаимодействовать с нашим хуком
NtUnloadKey
, вызывая эту функцию через ntdll.dll
. Юзермод простой, но выполняет свою задачу.
Чтение и запись памяти процесса
Из интересного, чтение памяти процесса реализованно с помощью ручного парсинга MMU уровней Page Table.
Мы получаем адрес
PML4
из структуры _EPROCESS
дальше мануально проходим по уровням пока не дойдем до Page Table где и находится физический адрес.В итоге мы не используем
KeAttachProcess
или MmCopyVirtualMemory
.Я возможно позже запишу статью на счет того как именно работает перевод адреса с вирт в физ.
Спасибо за внимание! Надеюсь, вы узнали что-то новое