Elysium - UEFI Bootkit Framework | by: Z3bra
Фреймворк UEFI Bootkit, который атакует механизм проверки целостности кода (Code Integrity) во время загрузки Windows, позволяя обойти защиту и загружать неподписанные драйверы, а также выполнять более сложные манипуляции с системой.
Введение
Разбирая winload.efi, было придумана идея пропатчить проверку сертификатов загрузочных драйверов, чтобы можно было загружать неподписанные драйверы без блокировки.
Целостность кода в winload.efi
Чтобы понять, как работает целостность кода в winload.efi, нужно разобраться, откуда загружаются драйверы.
Windows загружает драйверы в два основных этапа: winload.efi и ntoskrnl.exe.
В winload.efi загружаются Boot Start и ELAM драйверы.
Драйверы, запускаемые при загрузке, загружаются с помощью OslLoadDrivers в winload.efi, который анализирует реестр SYSTEM, чтобы перечислить все драйверы, помеченные Start = 0. Затем он отображает их в памяти и подготавливает их блоки данных загрузчика.
В OslLoadDrivers функция OslLoadImage обрабатывает фактическую загрузку изображения. Внутренне она вызывает LdrpLoadImage, затем BlImgLoadPEImageEx и, наконец, ImgpLoadPEImage.
В ImgpLoadPEImage вызывается функция ImgpValidateImageHash для проверки сертификата изображения по политике доверенной загрузки. Исправив эту функцию, мы можем полностью пропустить проверку сертификата.
Кроме того, функция ImgpLoadPEImage выполняет проверку контрольной суммы для обнаружения подделки. Точную логику можно увидеть в исходном коде ReactOS. Исправление этого позволяет нам модифицировать двоичный файл без необходимости пересчета контрольных сумм.
Вместе эти два патча открывают широкий спектр возможностей, выходящих далеко за рамки простой загрузки пользовательского драйвера.
1. Загрузка произвольных загрузочных драйверов
Как и предполагалось изначально, этот патч позволяет загружать драйверы независимо от их статуса подписи, включая неподписанные, тестовые или просроченные драйверы. Единственное требование к драйверу — он должен запускаться при загрузке.
Теоретически мы также можем загрузить наш собственный драйвер ELAM.
В примере проекта я создал пакетный скрипт, который автоматически создает службу драйвера. Скрипт устанавливает тип запуска драйвера как BOOT_START, обеспечивая его загрузку во время загрузки из winload.efi и обход проверки сертификата.
После выполнения скрипта все, что вам нужно сделать, это перезагрузить систему. После перезагрузки драйвер будет запущен автоматически.
2. Эмуляция драйверов загрузчика
Вот первая возможность, которая изначально не должна была существовать.
Мы можем заменить любой уже существующий драйвер запуска системы на наш собственный. Это позволяет нам захватить любой основной драйвер запуска системы: tcpip.sys, disk.sys, ACPI.sys и другие.
Да, мы можем захватить любой критически важный для системы драйвер запуска, но проблема заключается в следующем: как заставить систему продолжать функционировать без него?
Подход к решению этой проблемы — эмуляция драйвера. По сути, мы эмулируем поведение или функциональность критически важного драйвера, чтобы система продолжала «дышать».
Если система зависит от экспорта драйвера, мы можем его воспроизвести; если она зависит от конкретной функциональности драйвера, мы также можем имитировать это поведение.
Как остаться незамеченным для ELAM?
Да, мы можем перехватить и эмулировать любой драйвер, запускаемый при загрузке, но такой подход может быть потенциально обнаружен драйвером ELAM (Early Launch Anti-Malware).
ELAM предназначен для проверки драйверов, запускаемых при загрузке, на наличие распространенных сигнатур вредоносных программ и в основном используется антивирусным программным обеспечением для обнаружения вредоносной деятельности. Хотя он не должен затрагивать наш драйвер, поскольку он не является общеизвестной угрозой, теоретически ELAM может заметить наш недействительный сертификат в драйвере.
Драйвер ELAM загружается до всех драйверов, запускаемых при загрузке, но есть основные компоненты, которые загружаются еще раньше. Одним из таких компонентов является библиотека обновления микрокода, которая выбрана в качестве цели.
В данном случае техника эмуляции является идеальной, поскольку этот драйвер предоставляет один простой интерфейс, который используется системой, и одну экспортируемую переменную. Эмулируя его поведение, мы можем имитировать исходную функциональность и заставить систему поверить, что библиотека обновления работает нормально.
Система предоставляет две версии библиотеки обновления микрокода: одну для процессоров AMD (mcupdate_AuthenticAMD.dll) и одну для процессоров Intel (mcupdate_GenuineIntel.dll).
Если мы рассмотрим записи обоих драйверов, то увидим, что они предоставляют операционной системе очень похожий интерфейс:
С точки зрения компонентов системы, не имеет значения, какой производитель процессора используется; все, что им нужно, — это единый интерфейс для обновления микрокода.
В примере проекта эмулируется этот драйвер, чтобы добиться произвольного выполнения кода в отдельном потоке без нарушения работы системы. Перенаправляя функции структуры интерфейса на успешные гаджеты ROP, позволяя системе полагать, что функции обновления работают как положено.
Самая интересная часть — выяснить, как добиться выполнения кода во время работы. Эта библиотека выполняется еще до всех драйверов, запускаемых при загрузке, всего через несколько строк после загрузки самого образа ядра.
Когда эта библиотека запускается, мы все еще работаем в контексте прошивки, где еще не выделено заполнение памяти, и система все еще использует физическую память напрямую. На этом этапе ядра AP (Application Processor) еще даже не запущены, и мы работаем исключительно на ядре BSP (Bootstrap Processor). Это означает, что мы еще не можем просто создать поток, поскольку на этом этапе потоки еще не существуют.
Еще одна интересная деталь заключается в том, что точка входа этой библиотеки выполняется три раза во время процесса загрузки, причем два из этих вызовов поступают из ntoskrnl.exe. Теоретически мы могли бы подключиться к выполнению непосредственно из DriverEntry, но мы пойдем по другому пути.
Пришла в голову идея использовать одну из функций, предоставляемых этим интерфейсом, для получения права на выполнение. Были отслежены все эти вызовы, чтобы определить, какие из них выполняются во время процесса загрузки системы, сколько раз они запускаются и откуда.
В результате этого анализа было определено две функции интерфейса, которые вызываются ntoskrnl.exe во время загрузки. Одна из них, HalpMcUpdateExportData, вызывается непосредственно ntoskrnl.exe на этапе инициализации.
Как мы видим, ntoskrnl.exe вызывает экспортированную функцию из нашего интерфейса дважды, но только в том случае, если первый вызов возвращает STATUS_BUFFER_TOO_SMALL. Поскольку мы хотим выполнить его только один раз, мы просто вернем успех.
Последней проблемой перед выполнением кода во время работы является то, что мы не можем просто создать поток даже из этого выполнения. На данный момент мы все еще находимся на ранней стадии инициализации ntoskrnl.exe, когда структуры, связанные с потоками, еще не полностью инициализированы.
Однако мы можем обойти эту проблему, зарегистрировав обратный вызов уведомления о загрузке образа с помощью PsSetLoadImageNotifyRoutine. Этот обратный вызов будет запущен, когда будет загружен первый процесс в системе (обычно smss.exe). С этого момента можно безопасно создавать поток и продолжать выполнение.
На видео вы можете увидеть, как выглядит выполнение с стеком вызовов потока:
3. Бинарный бэкдор ядра
Здесь также раскрывается еще одна возможность, которая не планировалось использовать.
Bootkit исправляет функцию OslLoadImage, чтобы обеспечить загрузку неподписанных и исправленных образов. Интересно то, что ntoskrnl.exe загружается через эту функцию. Это означает, что мы можем не только исправлять ядро, но и, теоретически, перехватывать, эмулировать или даже подделывать его.
В данном случае, поскольку бинарный файл ядра может быть исправлен, был придуман подход, аналогичный тому, который использовался в бутките Insomnia. В Insomnia мы исправили SSDT, чтобы перенаправить syscall на наш пользовательский загрузчик. Здесь мы используем немного другой подход: мы можем исправить SSDT, чтобы перенаправить syscall в пользовательском режиме на другую функцию ядра.
Например, мы могли бы сделать так, чтобы вызов NtShutdownSystem из пользовательского режима вместо этого выполнял MmCopyVirtualMemory. 
В этом примере проекта автором создан скрипт на Python, который анализирует DIRECTORY_ENTRY_DEBUG файла PE для извлечения информации о файле PDB (база данных программы). Затем скрипт загружает соответствующий файл PDB с серверов Microsoft и извлекает адрес символа KiServiceTable.
Во время выполнения KiServiceTable содержит упакованные адреса функций системных вызовов, тогда как в исходном файле он содержит только смещения RVA. Скрипт исправляет заданную функцию системного вызова, чтобы перенаправить ее на другую указанную функцию. Единственное требование заключается в том, что как исходный системный вызов, так и целевая функция перенаправления должны быть экспортированы файлом PE. Скрипт автоматически собирает RVA из экспортов, поэтому для работы исправления необходимо экспортировать обе функции.
И, наконец, простой PoC, который демонстрирует использование перенаправленной функции NtShutdownSystem в DbgPrint, которая выводит сообщение, выполняя NtShutdownSystem из пользовательского режима.
4. Заражение драйвера ядра
Поскольку мы можем не только перехватить, но и исправить драйверы загрузки, автором придумано заразить их специально созданным образом.
Реализовано это: вставив пользовательские данные образа в качестве шелл-кода в новосозданный раздел с правами RWX, а затем обновив точку входа драйвера, чтобы она указывала на вход нашего образа.
Для этого проекта автор создал два компонента:
- Infector – заражает заданный легитимный драйвер полезной нагрузкой.
 - Payload – содержит фактическое выполнение заражения, которое запускается в ядре.
 
Infector скрывает исходную точку входа драйвера внутри заголовков PE, а именно в OptionalHeader.LoaderFlags. Во время выполнения мы извлекаем это значение и вызываем исходную точку входа, чтобы сохранить исходное выполнение драйвера.
Такой подход позволяет нам эффективно заражать драйвер ядра специально созданным образом. Этот метод можно применять к любому драйверу, запускаемому при загрузке, чтобы заразить его таким же образом.
Обнаружение, HVCI и Patch Guard
Этот буткит нацелен только на winload.efi, применяя свой патч на ранней стадии процесса загрузки. После ExitBootServices буткит полностью выгружается, не оставляя следов в памяти. В момент, когда winload.efi загружает драйверы загрузки, эти драйверы только загружаются в память, но не выполняются, поэтому они не могут обнаружить патч. 
HVCI (Hypervisor-protected Code Integrity) обеспечивает целостность кода в режиме ядра через CI.dll, но не контролирует и не защищает winload.efi. Это означает, что изменения в загрузчике могут полностью обойти проверки HVCI.
Patch Guard (Kernel Patch Protection) активируется только после полной инициализации ntoskrnl.exe. Поскольку буткит патчит winload.efi, он выполняется до активации Patch Guard, оставляя загрузчик незащищенным этим механизмом.
Оригинальная тема | Перевод: THREAD