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

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

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

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

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

Категории

Все категории
Loading categories
CVE-2026-54107 — Анализ первопричины CVE-2026-54107: use-after-free в Windows win32kfull.sys с отладкой условий гонки, статическим анализом, аналитикой триажа MSRC и практическими исследованиями эксплуатации ядра. | Kitploit
Инструменты/GitHubGitHub/pravin761/cve-2026-54107
Статический анализАнализ уязвимостейЭксплуатацияОбратная инженерияОтладчикиСтатьи и ИсследованияОбучение и ОбразованиеЭксплуатация Бинарных Файлов
GitHubpravin761/cve-2026-54107

CVE-2026-54107

Анализ первопричины CVE-2026-54107: use-after-free в Windows win32kfull.sys с отладкой условий гонки, статическим анализом, аналитикой триажа MSRC и практическими исследованиями эксплуатации ядра.

Репозиторий
131 месяц назадЕщё не проверено

Популярное

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

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

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

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

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

Когда ValidateHwnd — не ворота: поиск первопричины CVE-2026-54107

Use-after-free в win32kfull.sys при управлении жизненным циклом окон — как я его нашёл, как убедил себя, что он реален, и как на самом деле выглядел процесс MSRC со стороны исследователя.

CVE-2026-54107 CWE-362 CVSS 8.8 MSRC 11xxxxx Bounty $8,000

Было почти 2 часа ночи, когда целевая VM перестала отвечать на heartbeat отладчика и остановилась на break ровно на той инструкции, о достижимости которой я спорил неделями. Не assertion, не остановка из-за повреждённого пула — обычное нарушение доступа на пути диспетчеризации сообщений, при разыменовании объекта, который другой поток уже уничтожил.

Этот break стал CVE-2026-54107, делом MSRC , исправленным в для 27 продуктов Windows.

kd> !pool 2
11xxxxx
обновлении безопасности за июль 2026 года

Этот пост — не попавшая под эмбарго половина истории: первопричина, почему класс ошибки именно такой, и рассуждения, которые привели меня к ней. Детали эксплуатации остаются за кадром.

Содержание

  • 1. Почему win32k и почему именно оконные объекты
  • 2. Запашок, из-за которого я остановился
  • 3. Первопричина
  • 4. Почему оценка воздействия именно такая
  • 5. Сначала фальсификация — большинство кандидатов отсеялось
  • 6. Проверка: статический анализ даёт гипотезы, отладчик — истину
  • 7. Об использовании ИИ в исследованиях ядра
  • 8. Таймлайн MSRC, честно
  • 9. Итог
  • 10. Что бы я сказал новичку
  • 11. Что дальше

1. Почему win32k и почему именно оконные объекты

Win32k — это работающая в режиме ядра половина графической подсистемы Windows. Она старая, она огромная и — что критически важно — она достижима из контекстов, которые считаются недоверенными. Именно последнее свойство делает её постоянной целью исследований, несмотря на двадцать лет работ по ужесточению, фильтрации и ограничению системных вызовов.

Внутри win32k объект tagWND (PWND) необычайно интересен тем, что его временем жизни управляет сразу несколько механизмов. Окно:

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

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

2. Запашок, из-за которого я остановился

Что заставило меня засесть за этот компонент, так это поверхность импорта. win32kfull.sys тянет из ntoskrnl три различных примитива ссылок на объекты:```c NTSTATUS ObReferenceObjectByPointer( void *Object, uint32_t DesiredAccess, POBJECT_TYPE ObjectType, char AccessMode);

NTSTATUS ObReferenceObjectByHandle( HANDLE Handle, uint32_t DesiredAccess, POBJECT_TYPE ObjectType, char AccessMode, void **Object, OBJECT_HANDLE_INFORMATION *HandleInformation);

NTSTATUS ObReferenceObjectByName(/* ... */);

root@kitploit:~
Три пути входа, один путь выхода через `ObfDereferenceObject`.

Это не значит, что код неправильный. Это значит, что **инвариант распределён** — ни одна отдельная функция не владеет утверждением *«этот объект сейчас жив»*, поэтому корректность зависит от того, что каждый вызывающий код согласованно понимает, какой ссылкой он владеет и как долго эта ссылка действительна. Распределённые инварианты — это среда обитания состояний гонки, потому что гонка никогда не является логической ошибкой, которую можно увидеть в одной функции. Это ошибка в предположении, разделяемом между двумя.

Поэтому вопрос, который я начал задавать каждой функции, касающейся `PWND`, был не *«корректен ли этот код?»*, а:

> **Если это конкретное тело функции выполняется на двух потоках с интервалом в несколько инструкций, какой из них ошибочен?**



## 3. Корневая причина

Дефект представляет собой **разрыв между проверкой и использованием (time-of-check / time-of-use) между освобождением ссылки и уничтожением объекта** в пути уничтожения окна, без надлежащей синхронизации с параллельным потребителем, проверяющим валидность дескриптора.

Если свести к сути:```c
/* Thread A — teardown */
NtUserDestroyWindow(HWND hwnd)
{
    PWND pWnd = ValidateHwnd(hwnd);
    if (pWnd) {
        ObfDereferenceObject(pWnd);   /* reference released */

        /* <-- race window: object may become reclaimable here */

        FreeWindowObject(pWnd);       /* teardown proceeds on a pointer
                                         no longer guaranteed live      */
    }
}

Входной фрагмент пуст — переводить нечего.```c /* Thread B — consumer, concurrent / NtUserMessageCall(HWND hwnd, UINT msg, ...) { PWND pWnd = ValidateHwnd(hwnd); / may resolve a handle whose object is mid-teardown */

root@kitploit:~
if (pWnd->fnid == FNID_BUTTON)    /* use-after-free */
    ...

}

root@kitploit:~
Чтобы это имело значение, должны выполняться два условия, и оба выполнялись:

**(a) Окно реально.** `ValidateHwnd` — это шлюз, который должен обеспечивать безопасность доступа на основе дескрипторов. Если проверка может успешно пройти для объекта, уничтожение которого уже началось, то это не шлюз, а рекомендация.

**(b) Освобождённая память управляема атакующим.** Поля, читаемые сразу после проверки, включают `fnid`, который управляет диспетчеризацией сообщений. Решение о диспетчеризации, принятое на основе переиспользованной памяти, — это разница между *«ненадёжным крахом»* и *«нарушением границы безопасности»*. Именно это различие — вся причина, по которой это CWE-362 с воздействием EoP, а не баг стабильности.

> Наблюдаемое **повреждение** — это use-after-free; **причина** — CWE-362, параллельное выполнение с использованием общего ресурса без надлежащей синхронизации. Это два разных утверждения, и MSRC интересует второе. **Сообщайте о причине, а не только о симптоме.**

### Почему гонки в win32k структурно сложнее, чем кажутся

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

**Окна имеют привязку к потоку.** Окно принадлежит потоку, который его создал. Значительная часть подсистемы построена на предположении, что владеющий поток — это тот, кто обращается к объекту, а это означает, что наивный подход «запустить два потока, вызывающих один и тот же API», часто ничего не пересекает — вы не создаёте гонку, а выстраиваетесь в очередь. Чтобы два пути действительно столкнулись на одном объекте, нужно понимать, какие операции реально выполняются в потоке вызывающего, а какие маршализуются в поток владельца.

**Диспетчеризация сообщений частично сериализует вас.** Отправки (sends) и постановки в очередь (posts) ведут себя по-разному, а межпотоковая и внутрипоточная диспетчеризация — снова по-разному. Часть того, что выглядит как возможность параллелизма, молча превращается в упорядоченную операцию, прежде чем достигнет интересующего вас кода. Если вы не знаете, в какую категорию попадает ваш триггер, вы сделаете вывод, что настоящая гонка недостижима, — это ложноотрицательный результат, который неотличим от «здесь нет бага».

**Критическая секция прячется в вызывающем коде.** Значительная часть подсистемы работает под крупнозернистой блокировкой, захватываемой значительно выше функции, на которую вы смотрите. Это самый большой источник потерянного времени при аудите win32k: функция без видимой синхронизации, которая тем не менее полностью безопасна, потому что все пути в неё уже сериализованы. **Покрытие блокировкой — это межпроцедурное свойство.** Нужно подниматься по графу вызовов, а не просто читать функцию.

Именно из-за третьего пункта фраза *«в этой функции нет блокировки»* как сигнал почти ничего не стоит, и именно поэтому большая часть работы в этом исследовании была потрачена на достижимость, а не на сам дефект.

## 4. Почему оценка воздействия именно такая

MSRC оценило это как **Important, Elevation of Privilege, CVSS 8.8, вектор атаки — локальный, аутентифицированный.** Эту оценку определяют два свойства:

**Достижимость из низкой целостности.** Поверхность вызовов сообщений win32k достижима из контекстов, находящихся значительно ниже SYSTEM. Именно поэтому она актуальна для цепочек побега из песочницы: процесс рендеринга, уже добившийся выполнения кода внутри своей песочницы, всё ещё может обращаться к этой поверхности. Серьёзность ядерного бага — это в основном функция того, *кто может до него добраться*, а не того, насколько изощрённым является повреждение.

**Повреждение, влияющее на диспетчеризацию.** Повреждение поля, на котором работает `switch`, качественно хуже, чем повреждение поля, которое только лишь логируется. Первое превращает баг памяти в вопрос управления потоком выполнения.

Я хочу быть здесь точным, потому что видел, как авторы первых CVE преувеличивают это: **я продемонстрировал гонку и use-after-free. Я не предоставлял готового эксплойта уровня SYSTEM.** Формулировка «побег из песочницы» описывает *класс цепочек*, к которому относится этот тип бага, и почему эта поверхность ценна, — это аргумент о достижимости, а не утверждение, что я такой эксплойт построил. Преувеличение воздействия — самый быстрый способ сжечь доверие вендора, и оценка MSRC — это цифра, которая имеет значение, а не моя.

## 5. Сначала была фальсификация — большинство кандидатов отпали

Та часть, о которой никто не пишет: это был не первый кандидат. Это тот, который выжил.

Моё рабочее правило: **кандидат считается виновным, пока его виновность не доказана.** Для каждого перспективного паттерна записывается конкретная причина, по которой он *не должен* быть эксплуатируемым, и я сначала пытаюсь подтвердить эту причину, а уже потом — воспроизвести его. Кандидаты, которые я закрыл до этого, включали:

- пути, которые выглядели несинхронизированными, но были сериализованы блокировкой, захватываемой на один кадр выше,
- пути, где «освобождённый» объект на самом деле кэшировался, а не освобождался,
- пути, которые были действительно гонящимися, но недостижимыми из какого-либо вызывающего кода, доступного пользователю с низкими привилегиями.

Каждый из них — это находка, которую я *не* отправил в MSRC. В этом и суть. Продуктивность исследователя — это не количество сгенерированных кандидатов, а скорость, с которой он может отбраковать ошибочные, чтобы не держать их ещё в два часа ночи.

**Три вопроса, которые убили большинство кандидатов:**

1. **Держит ли кто-то выше меня блокировку?** Межпроцедурно, а не локально. Отсутствие блокировки в функции ничего не значит.
2. **Может ли непривилегированный вызывающий код реально достичь обеих сторон?** Гонка между двумя путями, требующими разных уровней привилегий, — это не гонка, а мысленный эксперимент.
3. **Можно ли переиспользовать освобождённую память в окне, на которое я могу влиять?** Если разрушение практически атомарно, то нет бага, о котором стоит сообщать.

## 6. Проверка: статический анализ даёт гипотезы, отладчик — истину

Статический анализ `win32kfull.sys` дал мне гипотезу. Он никогда не мог дать мне сам баг. **Гонки не видны в декомпиляторе**, потому что дефект не в инструкциях — он в чередовании выполнения.

### Лаборатория

| Роль | Конфигурация |
| --- | --- |
| Хост / отладчик | Windows 11, WinDbg |
| Цель | Windows Server 2022, Build 20348.2159 |
| Анализ | Kali Linux + Windows 11 VM |
| Транспорт отладки | VMware serial COM, отладка ядра хост → цель |
| Статический анализ | Ghidra via GhidraMCP |
| Помощь при триаже | Проход с помощью ИИ по декомпилированному выводу |

Реальную работу выполняли три слоя инструментирования.

### Special pool и Driver Verifier

Самый эффективный шаг в любом расследовании UAF в ядре. По умолчанию освобождённая память пула почти сразу переиспользуется следующим выделением похожего размера — а это значит, что use-after-free обычно *не вызывает сбой*. Он читает чужие валидные данные, продолжает выполнение и взрывается где-то в несвязанном месте минуты спустя. Затем вы тратите три дня на аудит невиновной функции.

Special pool меняет это. Каждое выделение получает собственную страницу с соседней guard-страницей, а освободившиеся страницы помечаются как no-access вместо переиспользования. В результате проблемное разыменование вызывает сбой **на той инструкции, которая его выполняет**, а не ниже по потоку:```
!verifier 0x1 win32kfull.sys        ; special pool on the target driver
!verifier 0x8 win32kfull.sys        ; pool tracking

В сочетании с фильтрацией по тегам пула именно это превращает «периодический bugcheck под нагрузкой» в воспроизводимый и атрибутируемый сбой.

Если вы усвоите из этого материала только одно: включайте специальный пул до старта, а не после того, как застряли.

Форензика пула при сбое

Как только у вас есть сбой, вопрос в том, имеете ли вы дело с повреждением или с ошибкой времени жизни. Для них нужны разные отчёты. Метаданные пула дают ответ:``` kd> !pool

root@kitploit:~
Блок, который *выделен* с правдоподобным тегом и мусорным содержимым, указывает на **повреждение**. Блок, который *освобождён* или находится на странице без доступа в специальном пуле, указывает на **ошибку времени жизни** — что-то удерживало указатель после смерти объекта. В этом различие между *«здесь писал атакующий»* и *«этот объект не должен был быть достижимым»*, и это разница между отчётом о повреждении кучи и отчётом CWE-362.

Перепроверьте тип объекта, прежде чем остановиться на каком-либо из вариантов. У `PWND` есть узнаваемая форма; если память, на которой произошёл сбой, всё ещё хранит его остатки, вы почти наверняка имеете дело с проблемой окна времени жизни, а не со случайной перезаписью.

### Живая отладка ядра при чередовании

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

Путь через это — перестать пытаться поймать гонку точкой останова и вместо этого:

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

Сам сбой, когда он у вас есть, выглядит буднично — разыменование `PWND` на пути диспетчеризации сообщений, где объект уже прошёл уничтожение в другом потоке, а `!pool` подтверждает, что блок был освобождён, а не перезаписан:```
kd> !analyze -v

EXCEPTION_CODE: (NTSTATUS) 0xc0000005 - Access violation
FAULTING_MODULE: win32kfull

(Смещения, адреса и детали воспроизведения намеренно опущены в рамках скоординированного раскрытия.)

Подтверждение тезиса о границе

Влияние не доказывается падением. Оно доказывается тем, кто может вызвать падение. Каждый запуск триггера выполнялся из стандартной учётной записи без прав администратора на целевой системе, потому что сбой ядра, достижимый только из уже привилегированного контекста, — это баг стабильности, а не безопасности. Проверка уровня целостности процесса, вызвавшего коллизию, — это шаг на тридцать секунд, который решает, есть ли у вас кейс для баунти или запись в Windows Feedback Hub.

Ключевая дисциплина: я не верил ни одному единичному падению. Единичное падение на гонке — это шум. Пригодным для отчёта его делала воспроизводимость при контролируемом тайминге — возможность сказать «эти два пути, такой порядок, такое окно» и получить тот же сбой. В этом разница между отчётом, с которым MSRC может работать, и тем, который закрывают как невоспроизводимый.

7. Об использовании ИИ в исследовании ядра

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

Хорош для: широкого охвата. Чтение большого объёма HLIL и пометка «эти функции обращаются к общему объекту без видимой синхронизации» — это сопоставление с образцом, а массовое сопоставление с образцом — именно то, что такие инструменты умеют хорошо. Это сжало недели беглого просмотра в дни.

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

Режим отказа, которого стоит бояться, — не в том, что инструмент ошибается. А в том, что он уверенно ошибается, в 2 часа ночи, когда тебе так хочется, чтобы он был прав.

Выдуманная находка, отправленная в MSRC, стоит их инженерам реального времени, а вам — репутации, которую быстро не восстановить.

8. Хронология MSRC, если честно

ДатаСобытие
May 7, 2026Отправлено — VULN-186460
May 7, 2026Кейс открыт — MSRC Case 11xxxxx
Jun 11, 2026Поведение подтверждено Microsoft; открыто рассмотрение баунти
Jun 27, 2026Исправление запланировано на июльский релиз; присвоен CVE-2026-54107 (до релиза)
Jun 30, 2026Вознаграждение присуждено — US$8,000, Windows Insider Preview Bounty Program
Jul 14, 2026Патч выпущен; CVE опубликован

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

Одна мелочь, заставившая меня посмеяться над собой: 14 июля по моему времени я написал в кейс, спрашивая, почему CVE ещё не опубликован. Мне вежливо ответили: в Сиэтле сейчас 13 июля. Календарь релизов Microsoft работает по тихоокеанскому времени. Теперь я знаю.


9. Сводка

ПолеДетали
CVECVE-2026-54107
Кейс MSRC11xxxxx (VULN-186460)
Компонентwin32kfull.sys — жизненный цикл объектов окна
КлассСостояние гонки → use-after-free
CWECWE-362
ВоздействиеПовышение привилегий
СерьёзностьImportant (MSRC)
CVSS v3.18.8 (High)
ВекторЛокальный, с аутентификацией
ПрограммаWindows Insider Preview Bounty Program
ВознаграждениеUS$8,000
ИсправленоОбновление безопасности за июль 2026 года

10. Что бы я сказал тому, кто начинает

Читай код в поисках инвариантов, а не багов. Вопрос «где этот код предполагает то, что сам не обеспечивает?» находит больше, чем «где переполнение?» — особенно в зрелых, тщательно аудируемых компонентах, где простые классы уязвимостей уже исчерпаны.

Гонка — это межпроцедурное утверждение. Её нельзя ни подтвердить, ни опровергнуть, глядя на одну функцию. Если твой анализ останавливается на границе функции, ты будешь генерировать кандидатов, которых никогда не закроешь.

Твоё отладочное окружение — это и есть работа. Я потерял больше часов на нестабильном последовательном COM-соединении, чем на самой охоте, а сломанный конвейер даёт ложноотрицательные результаты, которые выглядят точно так же, как «здесь ничего нет». Я чуть не бросил эту цель из-за настройки COM-порта.

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

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

11. Что дальше

Та же методология, другие поверхности атаки — tcpip.sys, afd.sys, clfs.sys. Я предпочёл бы, чтобы меня знали по совокупности работ, а не по одной удачной находке, и единственный способ этого добиться — продолжать отсеивать кандидатов быстрее, чем я их генерирую.

Если ты там же, где я был год назад — пришёл из веб-баунти, интересуешься работой с ядром, не уверен, тот ли ты человек, который способен на это, — ты узнаешь это, просто делая. Выбери один драйвер. Подключи отладчик. Читай медленно. Продолжай спрашивать, что будет, если выполнить это дважды.

Именно с этого всё по-настоящему начинается.

Автор: Pravin Choudhary (@pr4v1nx) — независимый исследователь в области offensive security. Передано в Microsoft в рамках скоординированного раскрытия. Детали эксплуатации, смещения и код воспроизведения намеренно не раскрываются.

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