Calypso - UEFI Bootkit под Windows

D2

Администратор
Регистрация
19 Фев 2025
Сообщения
4,380
Реакции
0
Проект делал в спешке, хотел успеть к новому году, но увы на несколько дней пришлось отложить разработку из-за проблем в техникуме.
И все же, С Новым Годом всех вас! :)

1736042561934.png


Github / Showcase
tl;dr
Написал простой буткит, цель была лучше понять как работает процесс загрузки системы и саму разработку UEFI.
И получился мне кажется первый буткит с поддержкой коммуникации через юзермод с открытым кодом.
Думаю если кто-то позже будет изучать и пытатся собрать что-то своё то ему будет от чего отталкиваться.

- Вес скомпилированного буткита 7 килобайт
- Написан на C++ и немного asm
- Коммуникация с юзермодом через хук функции в ядре (не рантайм сервисы)
- Обходит и не триггерит KPP (Kernel Patch Guard)
- Проект собирается за два клика через Visual Studio без костылей

Функции:
- Чтение/запись виртуальной памяти ядра
- Чтение/запись памяти процесса (MMU парсинг)
- Завершение процессов
- Повышение привилегий процессов до NT/SYSTEM (LPE)
(можно добавить почти любой функционал так как можно вызывать экспортируемые функции из ядра)
Спойлер: Минусы
Не использует вообще никаких структур, все оффсеты захардкожены, из-за этого поддерживает только Windows 10 22H2. (к каждому оффсету я добавил откуда он взят, на случай если кто-то захочет портировать его на другие версии)
Нету модулей по инфицированию, обхода secure boot и так далее, цель была понять саму работу, можно считать это за PoC если так удобнее.
Спойлер: Анализ, разбор и обьяснение работы
Анализ Буткита
1736042551361.png


Здесь мы видим графическое представление работы буткита на базовом уровне.
Это поможет лучше понять, что будет происходить дальше.

UefiMain
1736041006958.png


В UefiMain буткит выполняет две основные задачи:
Во-первых, сохраняет оригинальный адрес функции ExitBootServices, чтобы восстановить его позже, и устанавливает хук на ExitBootServices, перенаправляя вызовы в ExitBootServicesWrapper.
Во-вторых, создает событие SetVirtualAddressMap, которое будет объяснено позже.

ExitBootServices Wrapper (asm)
1736041012459.png


В ExitBootServicesWrapper цель — извлечь адрес возврата из регистра RSP.
Как только адрес возврата получен, выполнение передается функции ExitBootServicesHook.
Именно поэтому мы не можем использовать событие ExitBootServices — внутри события невозможно получить адрес возврата.

ExitBootServices Hook
1736041017322.png


В ExitBootServicesHook задача заключается в нахождении базового адреса winload.efi.
Поскольку ExitBootServices вызывается из winload.efi, и у нас есть его адрес возврата, мы знаем, что он указывает на область внутри winload.efi.
Исполняемые образы всегда загружаются с начала страницы памяти, поэтому базовый адрес всегда будет делится на 0x1000.
Кроме того, все исполняемые образы имеют заголовок DOS в начале, начинающийся с определенного magic значения.
Имея эту информацию, мы можем идти назад по памяти, страница за страницей, считывая первые байты каждой страницы и проверять значение DOS magic чтобы найти базовый адрес.

1736041022427.png


Следующим шагом является определение адреса OslArchTransferToKernel.
Почему именно OslArchTransferToKernel? Эта функция вызывается, когда winload.efi завершает работу, и передает адрес LoaderBlock.
В структуре LoaderBlock содержится список LoadOrderListHead, в котором находится адрес ntoskrnl.exe.
Для этого мы используем простой сканер по паттерну, чтобы найти адрес OslArchTransferToKernel, и устанавливаем на него хук.

SetVirtualAddressMap Event
1736041026594.png


Помните событие, созданное в UefiMain? Сейчас настало его время.
Цель этого события — преобразовать адрес нашего будущего хука из физического в виртуальный.
До этого момента система работает только с физической памятью без виртуального адресного пространства.
На следующем этапе я объясню детали.

OslArchTransferToKernel Hook
1736041030880.png


На этом этапе у нас есть адрес LoaderBlock, и мы обходим структуру LIST_ENTRY, чтобы найти базовый адрес ntoskrnl.exe.
Получив базовый адрес ntoskrnl.exe, следующий шаг — выбрать функцию в ядре для установки хука.

Я выбрал функцию NtUnloadKey по нескольким причинам.
1736041113963.png


Во-первых, мы хотим установить связь между пользовательским режимом и драйвером UEFI.
Для этого наша функция ядра должна вызываться как syscall из библиотеки пользовательского режима ntdll.dll.
123


Подсказка — часто функции, являющиеся syscall'ами ядра, начинаются с префикса Nt или Zw

Основная причина выбора именно NtUnloadKey в том, что она является оберткой для функции CmUnloadKey.
Что это значит?
Как вы, возможно, знаете, в Windows есть функция безопасности, называемая Kernel Patch Guard (KPP).
Ее задача — сканировать память ядра на наличие изменений и вызывать BSOD, если они обнаружены.
Мы обходим KPP, модифицируя ядро до его выполнения, чтобы Patch Guard сравнивал уже измененное ядро с тем, что в памяти.
Однако проблема возникает при использовании хука с "трамплином".
Когда хук установлен, функция сначала прыгает на хук, выполняет свою работу, восстанавливает измененные байты, вызывает оригинальную функцию, а затем снова применяет "трамплин".
С активным KPP мы не можем удалять хук, так как это приведет к изменению ядра во время выполнения и вызовет синий экран от KPP.
Поэтому нам нужно найти способ заменить функционал оригинальной функции, не вызывая ее напрямую.
Функция враппер, как NtUnloadKey, идеально подходит для этого, так как чтобы восстановить оригинальный функционал нам надо просто передать параметры в другую функцию ядра.

NtUnloadKey Hook
1736041118507.png


В этой функции мы проверяем, соответствует ли переданный параметр нашей структуре команды. Если нет, мы возвращаем выполнение в CmUnloadKey, имитируя поведение оригинальной функции.
Если это наша команда, выполнение передается в dispatcher. (обработчик комманд)

Анализ пользовательского режима
1736041122182.png


Как мы помним, функция NtUnloadKey из ntdll.dll является syscall'ом в NtUnloadKey находящийся в ntoskrnl.exe.
Таким образом, usermode может взаимодействовать с нашим хуком NtUnloadKey, вызывая эту функцию через ntdll.dll.
Юзермод простой, но выполняет свою задачу.

Чтение и запись памяти процесса
1736091150219.png


Из интересного, чтение памяти процесса реализованно с помощью ручного парсинга MMU уровней Page Table.
Мы получаем адрес PML4 из структуры _EPROCESS дальше мануально проходим по уровням пока не дойдем до Page Table где и находится физический адрес.
В итоге мы не используем KeAttachProcess или MmCopyVirtualMemory.
Я возможно позже запишу статью на счет того как именно работает перевод адреса с вирт в физ.


Спасибо за внимание! Надеюсь, вы узнали что-то новое :)
 
Сверху Снизу