Проект делал в спешке, хотел успеть к новому году, но увы на несколько дней пришлось отложить разработку из-за проблем в техникуме.
И все же, С Новым Годом всех вас!
Github / Showcase
tl;dr
Написал простой буткит, цель была лучше понять как работает процесс загрузки системы и саму разработку UEFI.
И получился мне кажется первый буткит с поддержкой коммуникации через юзермод с открытым кодом.
Думаю если кто-то позже будет изучать и пытатся собрать что-то своё то ему будет от чего отталкиваться.
- Вес скомпилированного буткита 7 килобайт
- Написан на C++ и немного asm
- Коммуникация с юзермодом через хук функции в ядре (не рантайм сервисы)
- Обходит и не триггерит KPP (Kernel Patch Guard)
- Проект собирается за два клика через Visual Studio без костылей
Функции:
- Чтение/запись виртуальной памяти ядра
- Чтение/запись памяти процесса (MMU парсинг)
- Завершение процессов
- Повышение привилегий процессов до NT/SYSTEM (LPE)
(можно добавить почти любой функционал так как можно вызывать экспортируемые функции из ядра)
И все же, С Новым Годом всех вас!
Github / Showcase
tl;dr
Написал простой буткит, цель была лучше понять как работает процесс загрузки системы и саму разработку UEFI.
И получился мне кажется первый буткит с поддержкой коммуникации через юзермод с открытым кодом.
Думаю если кто-то позже будет изучать и пытатся собрать что-то своё то ему будет от чего отталкиваться.
- Вес скомпилированного буткита 7 килобайт
- Написан на C++ и немного asm
- Коммуникация с юзермодом через хук функции в ядре (не рантайм сервисы)
- Обходит и не триггерит KPP (Kernel Patch Guard)
- Проект собирается за два клика через Visual Studio без костылей
Функции:
- Чтение/запись виртуальной памяти ядра
- Чтение/запись памяти процесса (MMU парсинг)
- Завершение процессов
- Повышение привилегий процессов до NT/SYSTEM (LPE)
(можно добавить почти любой функционал так как можно вызывать экспортируемые функции из ядра)
Не использует вообще никаких структур, все оффсеты захардкожены, из-за этого поддерживает только Windows 10 22H2. (к каждому оффсету я добавил откуда он взят, на случай если кто-то захочет портировать его на другие версии)
Нету модулей по инфицированию, обхода secure boot и так далее, цель была понять саму работу, можно считать это за PoC если так удобнее.
Нету модулей по инфицированию, обхода 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.
Мы получаем адрес
В итоге мы не используем
Я возможно позже запишу статью на счет того как именно работает перевод адреса с вирт в физ.
Спасибо за внимание! Надеюсь, вы узнали что-то новое
Здесь мы видим графическое представление работы буткита на базовом уровне.
Это поможет лучше понять, что будет происходить дальше.
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.Я возможно позже запишу статью на счет того как именно работает перевод адреса с вирт в физ.
Спасибо за внимание! Надеюсь, вы узнали что-то новое