
Эксплойт Proof-of-concept для CVE-2026-29923, повышение привилегий BYOVD в pstrip64.sys. Демонстрирует чтение/запись физической памяти через IOCTL для кражи токена SYSTEM и запуска оболочки с повышенными привилегиями.
Отказ от ответственности: Этот код предоставлен только для образовательных целей и исследований в области защиты. Он был написан для углубления понимания эксплуатации ядра и помощи защитникам в предотвращении подобных уязвимостей. Любое несанкционированное, незаконное или злонамеренное использование этого проекта строго запрещено.
Хеш: ab01485bb7c8bc1a9c86096eeea6d31d8fad557bf4d44072b46373d2203faa6e
Имя драйвера: pstrip64.sys
CVE: CVE-2026-29923
Атака «Принеси свой собственный уязвимый драйвер» (BYOVD) — старый, но по-прежнему эффективный способ обойти современные средства защиты Windows, используя устаревший драйвер, которому операционная система всё ещё официально доверяет. После загрузки драйвера злоумышленник использует его недостатки, чтобы преодолеть разрыв между стандартным непривилегированным процессом и полным контролем над системой.
Ранее на этой неделе была раскрыта новая уязвимость в драйвере pstrip64.sys, отслеживаемая под идентификатором CVE-2026-29923. Эта запись в блоге подробно описывает весь жизненный цикл эксплойта: от моих первоначальных исследований уязвимости и разработки доказательства концепции (PoC) до практических стратегий смягчения последствий для защитников, обеспечивающих безопасность своих сред.
Драйвер pstrip64.sys — это устаревший компонент режима ядра, связанный с EnTech Taiwan PowerStrip (до версии 3.90.736). Хотя его законное назначение — обеспечение расширенных возможностей настройки отображения графических карт, его глубокие системные привилегии делают его весьма привлекательной целью для атакующих.
Когда уязвимость была впервые раскрыта, я начал с анализа её функции DriverEntry. Она служит основной процедурой инициализации драйвера ядра, создавая объект устройства \Device\PSTRIP64 и открывая его для пользовательских приложений через символическую ссылку \DosDevices\PSTRIP64. Что ещё более важно, она настраивает диспетчерскую таблицу драйвера. Запись, которая сразу привлекла моё внимание, находилась под индексом 14 (IRP_MJ_DEVICE_CONTROL), которая направляет все предоставленные пользователем запросы IOCTL непосредственно в функцию-обработчик sub_11340 — нашу основную область интереса.
Функция sub_11340 служит основным диспетчером IOCTL, интерпретируя запросы из пользовательского режима.
Из всех доступных IOCTL, 0x80002008, несомненно, самый интересный. В то время как стандартные случаи обрабатывают незначительные взаимодействия с портами ввода-вывода, 0x80002008 действует как шлюз к sub_11000, передавая SystemBuffer напрямую в эту функцию.
Эта процедура sub_11000 — неопровержимая улика. Сначала она использует HalTranslateBusAddress для преобразования предоставленного пользователем адреса в действительный системный физический адрес. Затем она открывает \Device\PhysicalMemory и отображает его с помощью ZwMapViewOfSection. Жёстко прописав дескриптор целевого процесса как (HANDLE)0xFFFFFFFFFFFFFFFFLL (что соответствует ZwCurrentProcess()), драйвер отображает эту физическую память непосредственно в адресное пространство виртуальной памяти вызывающего процесса. Критически важно, что затем он записывает этот только что отображённый виртуальный адрес обратно в SystemBuffer для возврата пользователю, официально передавая нашему приложению прямой указатель на чтение и запись физической памяти.
Полностью поняв уязвимость и получив примитив физического чтения/записи, я собрал все необходимые фрагменты головоломки. Теперь пришло время приступить к написанию доказательства концепции.
Примечание: Это PoC было специально разработано и протестировано в среде Windows 10 22H2. Поскольку эксплойт основан на манипуляции сырой физической памятью, смещения структур ядра и границы физической памяти в данный момент жёстко прописаны для моей конфигурации. Чтобы протестировать это на вашей машине, необходимо обновить смещения ядра Windows и настроить диапазоны сканирования физических адресов в соответствии с вашей конкретной сборкой ОС и конфигурацией ОЗУ.
Первым шагом в моём эксплойте было установление связи с драйвером. Я сделал это, вызвав CreateFileA для символической ссылки драйвера (\\.\PSTRIP64). Получив действительный дескриптор, мне понадобился чистый способ злоупотребить ранее проанализированным IOCTL 0x80002008. Я создал функцию-обёртку под названием MapPhysicalMemory(). Эта функция заполняет мой пользовательский структур PSTRIP_MAP_REQUEST целевым физическим адресом и длиной фрагмента памяти, который я хочу прочитать.
Затем я отправляю эту структуру напрямую драйверу через DeviceIoControl. В случае успеха драйвер отображает эту физическую память непосредственно в моё пользовательское приложение и возвращает базовый виртуальный адрес в поле OutputResult. Теперь я могу привести этот возвращённый адрес к стандартному указателю C++, что даёт мне необработанный непривилегированный доступ к физической ОЗУ системы.
После того как мой примитив физического чтения/записи полностью заработал, моей целью стало найти структуры данных ядра, содержащие привилегии процессов. В Windows каждый запущенный процесс представлен структурой EPROCESS.
Windows выделяет структуры EPROCESS в пуле ядра, используя определённый 4-байтовый идентификатор, называемый тегом пула. Для процессов этот тег — строка Proc (которая в шестнадцатеричном виде преобразуется в 0x636F7250). Сканируя физическую ОЗУ системы, я мог найти эту точную строку.
Мой эксплойт циклически перебирает пространство физической памяти от 0x10000000 до 0x140000000, отображая память блоками по 2 МБ (STEP_SIZE = 0x200000). Каждый отображённый блок я привожу к сырому массиву байтов и сканирую его блоками по 16 байт (sizeof(_POOL_HEADER)).
Однако простого поиска тега Proc в физической памяти недостаточно. Память неоднородна; этот тег может быть остаточным артефактом завершённого процесса или просто случайными данными, которые совпадают с шестнадцатеричным значением. Если бы я слепо предполагал, что каждый тег Proc является действительной структурой EPROCESS, и начал изменять память, я бы немедленно вызвал BSOD.
Чтобы обеспечить стабильность, мне пришлось проверить структуру с помощью эвристик. Сначала я вычисляю начало структуры EPROCESS (которое находится с небольшим смещением от тега пула). Оттуда я проверяю несколько известных констант для работающего процесса:
0x2 (Normal Priority).0x0.Если все эти эвристики проходят, я могу с высокой степенью уверенности утверждать, что смотрю на действительный активный процесс. Затем я читаю его уникальный идентификатор процесса (PID). Если PID совпадает с PID моего собственного процесса эксплойта, я сохраняю физический адрес его указателя на токен. Если PID равен 4 (системный процесс Windows System), я извлекаю и сохраняю фактическое значение его высокопривилегированного токена.
Наконец, я выравниваю сохранённый физический адрес указателя токена моего процесса до ближайшей границы 4 КБ и использую MapPhysicalMemory() в последний раз, чтобы отобразить только эту конкретную страницу.