
Доказательство концепции CVE-2025-24990 (драйвер Agere Systems)
ltmdm64.sys). Этот драйвер очень старый и не загружается по умолчанию на моей тестовой машине, поэтому я буду эксплуатировать его в сценарии BYOVD. Интересно, что, согласно моим исследованиям, этот драйвер существовал на Windows 7 и имеет как минимум одну ошибку. здесь
В то время MSRC не предпринял никаких действий 🤡
Некоторые IOCTL в этом драйвере используют METHOD_NEITHER, но не проверяют, находится ли буфер адреса, предоставленный вызывающим абонентом, в пользовательском или режиме ядра. Вот пример кода IOCTL, который я декодировал с помощью OSR:
Это означает, что вы можете передать адрес ядра в API DeviceIoControl, и драйвер обработает его обычным образом.
Обратите внимание, что сначала нужно обойти kASLR, чтобы утечь адрес ядра. Я буду использовать EnumDeviceDrivers (в Windows 24h2 для этого нужен SeDebugPriv).
Проблема в IOCTL 0x802b200f (ud_response). Опять же, этот диспетчер IOCTL не проверяет адрес, который я передаю из пользовательского режима, но я воспользуюсь этим позже.
ud_response вызывает ll_load_diagnostics, и я достигну следующего кода:
В начале глобальная переменная eeprom не инициализирована, поэтому она будет содержать NULL. Вот простой код, который вызовет это.
Я воспользуюсь этим позже.
Этот IOCTL просто преобразует строку версии драйвера "8.36" в число 0x836 (тип DWORD) и записывает его по адресу, предоставленному вызывающим абонентом (благодаря METHOD_NEITHER). Технически я могу записать эти четыре байта (36 08 00 00) по произвольному адресу ядра. Я воспользуюсь этим, чтобы перезаписать глобальные переменные драйвера и изменить поток выполнения.
Я буду называть этот 0x802b2003 IOCTL_GET_VERSION
Произвольный нулевой 1 байт:
Возвращаясь к случаю разыменования NULL, я использую API VirtualAlloc для выделения фиксированного адреса (0x083600000000). Затем я использую IOCTL_GET_VERSION, чтобы записать в *(eeprom + 4) четыре байта, описанные выше. Когда драйвер позже разыменует eeprom, он прочитает данные по адресу, который я выделил.
После исправления разыменования NULL, IOCTL записывает строку по адресу, который я передаю из пользовательского режима, на основе размера буфера.
Этот код просто демонстрирует то, что я описал выше: выделяет буфер и заполняет его 0xAA, исправляет разыменование NULL, затем вызывает драйвер. Обратите внимание, что я выделяю 11 байт, но передаю драйверу размер буфера только 10, чтобы посмотреть, как он себя поведет.
Он записывает фиксированную последовательность байтов в мой буфер, а затем обнуляет последний байт (11-й), даже если я предоставляю размер только 10. Он заменяет последний 0xAA в моем буфере на 0x00. Это указывает на то, что если я укажу размер 0, драйвер все равно запишет один байт 0x00 по целевому адресу.
Произвольное уменьшение
Теперь у меня есть нулевой и произвольный с фиксированными 4 байтами, давайте создадим другой примитив.
Этот IOCTL установит глобальный LtMsgEvent в мой пользовательский буфер, затем проверит, является ли WDM нулевым, и снова установит ноль.
Затем в 0x802b2207 он вызовет API ObfReferenceObject.
В начальном состоянии WDM равен нулю, но с помощью IOCTL_GET_VERSION я могу установить WDM в 0x36 (его размер всего 1 байт), а LtMsgEvent все еще мой буфер. Затем я обнулю WDM и вызову 0x802b2207. Наконец, достигну ObfReferenceObject. Я буду называть эти два ioctl IOCTL_SET_LtMsgEvent и IOCTL_DEREF_LtMsgEvent.
Техника эксплойта с использованием ObfReferenceObject изменяет PreviousMode нашего KTHREAD с UserMode на KernelMode. Вы можете прочитать об этом здесь. Однако Windows исправила этот эксплойт, поэтому мы не можем его использовать.
Но примитив в ObfReferenceObject все еще существует. API вычитает 0x30 из предоставленного нами адреса, приводит результат к 8-байтовому целому числу, а затем вычитает 1.
*(signed long long)(LtMsgEvent-0x30) -= 1
Но проблема в том, что он проверяет, является ли следующее значение 0 или текущее значение < 1 (интерпретируется как 8-байтовое целое число со знаком). Если какое-либо из условий истинно, он переходит к KeBugCheckEx и вызывает сбой системы.
Произвольная запись
Имея в руках произвольное уменьшение, мне нужно найти другое место для записи байта 0xFF, а затем уменьшить его до нужного мне байта, и я нашел этот ioctl 0x802b2243:
Мы сосредоточимся на ветке flip. pbVar5 — это адрес, который я передаю из пользовательского режима, и он может быть любым целевым адресом, который я выберу. Я записываю байт 0x0C в DAT_TARGET_EX (с помощью IOCTL_GET_VERSION и примитива произвольного уменьшения), а также обнуляю 1 байт по целевому адресу. Первый вызов этого IOCTL устанавливает 0xC0 по целевому адресу, который затем уменьшается до 0xBF. Второй вызов устанавливает 0xFF по целевому адресу (0xBF | 0xC0 = 0xFF). Как только цель содержит 0xFF, я просто уменьшаю его до желаемого значения.
Я буду записывать по одному байту за раз и быть осторожным с KeBugCheckEx в ObfReferenceObject.
Произвольное чтение
Для примитива чтения я использую технику, описанную здесь (@carrot_c4k3). Я просто перезаписываю объект UNICODE_STRING в ядре (ExpManufacturingInformation), а затем вызываю NtQuerySystemInformation. Из-за того, что ObfReferenceObject вызывает KeBugCheckEx, я обнулю 8 байт, смежных с ExpManufacturingInformation.
Вот и все, теперь у нас есть произвольное чтение/запись, и мы можем использовать эти примитивы для многих вещей. Драйвер не загружается по умолчанию, поэтому я буду эксплуатировать его в сценарии BYOVD и установлю PPL процесса.
Эксплойт, который я описал выше, работает во всех версиях Windows, но нестабилен из-за KeBugCheckEx. Но в Windows 11 22h2+ есть техника, называемая ioring. Эта техника просто перезаписывает ioring->Buffer управляемым адресом. Конкретно, мы можем перезаписать ioring->Buffer и его размер значениями 0x083600000000 и 0x836 соответственно (используя IOCTL_GET_VERSION). Используя эту технику, я выполняю только 2 записи, а затем использую примитив чтения/записи очень стабильно. Обратите внимание, что этот подход требует утечки адреса ядра.
Windows не загружает драйвер в состоянии по умолчанию. Поэтому вам нужно загрузить его вручную. Файл ltmdm64.sys находится в C:\Windows\System32\DriverStore\...\ltmdm64.sys. Выполните эту команду от имени администратора и запустите эксплойт:
sc create ltmdm64_srv binPath="C:\Windows\System32\DriverStore...\ltmdm64.sys" type=kernel && sc start ltmdm64_srv
Эксплойт будет использовать технику ioring для отключения PPL процесса lsass.exe и мою технику, основанную только на данных, для установки PPL для notepad.exe (в win 11 24h2 требуется, чтобы SeDebugPriv был включен)
https://github.com/user-attachments/assets/05a35b38-d26c-484f-9fb7-137f8fe8c079
Я сообщил об этой ошибке в ZDI. Но, похоже, она дублируется с заявкой Fabian Mosch и Jordan Jay в MSRC, поэтому этот PoC просто демонстрирует ошибку и ценит их работу. Почти мой первый CVE 😍