На Recon Montreal 2018 я вместе с Алексом Ионеску представил "Неизвестные известные библиотеки DLL и другие нарушения доверия к целостности кода". Мы описали реализацию механизмов целостности кода Microsoft Windows и то, как Microsoft реализовала защищенные процессы (PP). В рамках этого я продемонстрировал различные способы обхода Protected Process Light (PPL), некоторые из которых требуют прав администратора, другие нет.
В этой статье я собираюсь описать процесс, через который я прошел, чтобы обнаружить способ внедрения кода в PPL в Windows 10 1803. Поскольку единственная проблема, которую Microsoft считала нарушающей защищенные границы безопасности, теперь решена, я могу обсудить эксплойт более подробно.
Справочная информация о защищенных процессах Windows
Истоки модели защищенного процесса Windows (PP) уходят корнями в Vista, где она была введена для защиты процессов DRM. Модель защищенного процесса была сильно ограничена, ограничивая загруженные библиотеки DLL подмножеством кода, установленного с операционной системой. Также, чтобы исполняемый файл считался подходящим для запуска, он должен быть подписан с помощью специального сертификата Microsoft, встроенного в двоичный файл. Одна защита, которую обеспечивает ядро, заключается в том, что незащищенный процесс не может открыть дескриптор защищенного процесса с достаточными правами для ввода произвольного кода или чтения памяти.
В Windows 8.1 был представлен новый механизм Protected Process Light (PPL), который сделал защиту более универсальной. PPL ослабил некоторые ограничения на то, какие библиотеки DLL считались допустимыми для загрузки в защищенный процесс, и ввел различные требования к подписи для основного исполняемого файла. Еще одним большим изменением стало введение набора уровней подписи для разделения различных типов защищенных процессов. PPL на одном уровне может открыть для полного доступа любой процесс на том же уровне подписи или ниже, с ограниченным набором доступа, предоставленным уровням выше. Эти уровни подписи были распространены на старую модель PP, PP на одном уровне может открывать все PP и PPL на том же уровне подписи или ниже, однако обратное не верно, PPL никогда не может открывать PP на любом уровне подписи для полного доступ. Некоторые из уровней и эти отношения показаны ниже:
Уровни подписи позволяют Microsoft открывать защищенные процессы для третьих сторон, хотя в настоящее время единственный тип защищенного процесса, который может создать третье лицо, - это PPL Anti-Malware. Уровень защиты от вредоносных программ является особым, поскольку он позволяет третьей стороне добавлять дополнительные разрешенные ключи подписи путем регистрации сертификата раннего запуска защиты от вредоносных программ (ELAM). Существует также TruePlay от Microsoft, технология Anti-Cheat для игр, использующая компоненты PPL, но это не очень важно для этого обсуждения.
Я мог бы потратить большую часть этого сообщения в блоге на описание того, как PP и PPL работают под капотом, но я рекомендую вместо этого прочитать серию сообщений в блоге Алекса Ионеску, которые помогут лучше разорабраться. Хотя сообщения в блоге в основном основаны на Windows 8.1, большинство концепций в Windows 10 существенно не изменились.
Я уже писал о защищенных процессах до [https://googleprojectzero.blogspot.com/2017/08/bypassing-virtualbox-process-hardening.html] в виде специальной реализации Oracle в их платформе виртуализации VirtualBox в Windows. В блоге показано, как я обошел защиту процесса, используя несколько различных методов. То, о чем я не упомянул в то время, было первой описанной мною техникой, заключающейся в внедрении кода JScript в процесс, которая также работала против реализации PPL от Microsoft. Я сообщил, что могу внедрить произвольный код в PPL для Microsoft (см. проблему 1336) из предосторожности на случай, если Microsoft захочет исправить это. В этом случае Microsoft решила, что это не будет исправлено как бюллетень безопасности. Однако Microsoft исправила проблему в следующем крупном выпуске Windows (версия 1803), добавив следующий код в CI.DLL, библиотеку целостности кода ядра:
Исправление проверяет исходное имя файла в разделе ресурсов загружаемого образу по черному списку из 5 DLL. Черный список включает библиотеки DLL, такие как JSCRIPT.DLL, который реализует исходный механизм сценариев JScript, и SCROBJ.DLL, который реализует объекты скриптлетов. Если ядро обнаруживает, что PP или PPL загружает одну из этих DLL, загрузка изображения отклоняется с помощью STATUS_DYNAMIC_CODE_BLOCKED. Это убивает мой эксплойт, если вы измените раздел ресурсов одной из перечисленных DLL, подпись изображения станет недействительной, что приведет к сбою загрузки изображения из-за несоответствия криптографического хэша. Фактически это то же самое исправление, которое Oracle использовала для блокировки атаки в VirtualBox, хотя оно было реализовано в пользовательском режиме.
Поиск новых целей
Предыдущий метод внедрения с использованием кода сценария был общим методом, который работал с любым PPL, загружающим COM-объект. С исправленной техникой я решил вернуться и посмотреть, какие исполняемые файлы будут загружаться как PPL, чтобы увидеть, есть ли в них какие-либо очевидные уязвимости, которые я мог бы использовать для выполнения произвольного кода. Я мог бы выбрать полный PP, но PPL казался более простым из двух, и мне нужно с чего-то начать. Существует так много способов внедрения в PPL, если бы мы могли просто получить права администратора, наименьший из которых - это просто загрузка драйвера ядра. По этой причине любая обнаруженная мной уязвимость должна работать с учетной записью обычного пользователя. Также я хотел получить наивысший уровень подписи, который я мог получить, что означает PPL на уровне подписи Windows TCB.
Первым шагом было определение исполняемых файлов, которые запускаются как защищенный процесс, что дает нам максимальную поверхность атаки для анализа уязвимостей. Судя по сообщениям в блоге от Алекса, казалось, что для загрузки как PP или PPL сертификат подписи требует специального идентификатора объекта (OID) в расширении расширенного использования ключа (EKU) сертификата. Есть отдельные OID для PP и PPL; мы можем увидеть это ниже, сравнив WERFAULTSECURE.EXE, который может работать как PP/ PPL, и CSRSS.EXE, который может работать только как PPL.
Я решил поискать исполняемые файлы, которые имеют встроенную подпись с этими OID EKU, и это даст мне список всех исполняемых файлов для поиска уязвимого поведения. Я написал командлет Get-EmbeddedAuthenticodeSignature для моего модуля NtObjectManager PowerShell для извлечения этой информации.
На этом этапе я понял, что существует проблема с подходом к использованию сертификата подписи, я ожидал, что есть много двоичных файлов, которым будет разрешено работать как PP или PPL, которые отсутствовали в списке, который я создал. Поскольку PP изначально был разработан для DRM, не было очевидного исполняемого файла для обработки пути защищенного носителя, такого как AUDIODG.EXE. Кроме того, основываясь на моем предыдущем исследовании Device Guard и Windows 10S, я знал, что в платформе .NET должен быть исполняемый файл, который мог бы работать как PPL для добавления кэшированной информации об уровне подписи в сгенерированные NGEN двоичные файлы (NGEN - это Ahead-of-Time JIT для преобразования сборки .NET в собственный код). Критерии PP/PPL оказались более гибкими, чем я ожидал. Вместо статического анализа я решил выполнить динамический анализ, просто для начала защищал каждый исполняемый файл, который я мог перечислить, и запрашивал предоставленный уровень защиты. Я написал следующий сценарий для тестирования одного исполняемого файла:
При выполнении этого сценария определяется функция Test-ProtectedProcess. Функция берет путь к исполняемому файлу, запускает этот исполняемый файл с указанным уровнем защиты и проверяет, был ли он успешным. Если параметры ProtectedType и ProtectedSigner равны 0, тогда ядро определяет "лучший" уровень процесса. Это приводит к некоторым неприятным особенностям, например, SVCHOST.EXE явно помечен как PPL и будет работать на уровне PPL-Windows, однако, поскольку это также подписанный компонент ОС, ядро определит, что его максимальный уровень – PP-Authenticode. Еще одна интересная особенность заключается в том, что использование собственных API-интерфейсов создания процессов позволяет запускать DLL в качестве основного исполняемого образа. Поскольку значительное количество системных библиотек DLL имеют встроенные подписи Microsoft, их также можно запускать как PP-Authenticode, хотя это не обязательно так полезно. Список двоичных файлов, которые будут работать на PPL, показан ниже вместе с их максимальным уровнем подписи.
Внедрение произвольного кода в NGEN
Внимательно просмотрев список исполняемых файлов, работающих как PPL, я остановился на попытке атаковать ранее упомянутый двоичный файл .NET NGEN, MSCORSVW.EXE. Мое объяснение выбора бинарного файла NGEN было:
- Большинство других двоичных файлов представляют собой служебные двоичные файлы, которым для правильного запуска могут потребоваться права администратора.
- Бинарный файл, вероятно, будет загружать сложные функции, такие как .NET framework, а также иметь несколько взаимодействий COM (моя технология для странного поведения).
- В худшем случае это все равно может привести к обходу Device Guard, поскольку причина, по которой он работает как PPL, заключается в том, чтобы предоставить ему доступ к API ядра для применения кэшированного уровня подписи. Любая ошибка в работе этого двоичного файла может быть использована, даже если мы не можем запустить произвольный код в PPL.
Но есть проблема с двоичным файлом NGEN, в частности, он не соответствует моим критериям, согласно которым я получаю высший уровень подписи, Windows TCB. Однако я знал, что, когда Microsoft исправила проблему 1332, они ушли с черного хода, где во время процесса подписи можно было сохранить доступный для записи дескриптор, если вызывающим процессом является PPL, как показано ниже:
Если бы я мог получить выполнение кода внутри двоичного файла NGEN, я мог бы повторно использовать этот бэкдор для кеширования подписи произвольного файла, который будет загружаться в любой PPL. Тогда я мог бы перехватить DLL полный процесс PPL-WindowsTCB, чтобы достичь своей цели.
Чтобы начать расследование, нам нужно определить, как использовать исполняемый файл MSCORSVW. Использование MSCORSVW нигде не задокументировано Microsoft, поэтому нам придется немного покопаться. Во-первых, этот двоичный файл не должен запускаться напрямую, вместо этого он вызывается NGEN при создании двоичного файла NGEN. Таким образом, мы можем запустить двоичный файл NGEN и использовать такой инструмент, как Process Monitor, чтобы узнать, какая командная строка используется для процесса MSCORSVW. Выполнение команды:
C:\> NGEN install c:\some\binary.dll
Результат приводит к выполнению следующей командной строки:
MSCORSVW -StartupEvent A -InterruptEvent B -NGENProcess C -Pipe D
A, B, C и D - это дескрипторы, которые NGEN гарантирует, что они будут унаследованы в новый процесс перед его запуском. Поскольку мы не видим никаких исходных параметров командной строки NGEN, похоже, что они передаются через механизм IPC. Параметр "Pipe" указывает на то, что именованные каналы используются для IPC. Копаясь в коде MSCORSVW, мы находим метод NGenWorkerEmbedding, который выглядит следующим образом:
Этот код не совсем то, что я ожидал. Вместо использования именованного канала для всего канала связи он используется только для передачи маршалированного COM-объекта обратно вызывающему процессу. COM-объект - это экземпляр фабрики классов, обычно вы регистрируете фабрику с помощью CoRegisterClassObject, но это сделает его доступным для всех процессов с одинаковым уровнем безопасности, поэтому вместо этого с помощью маршалинга соединение можно оставить частным только для двоичного файла NGEN, который породил MSCORSVW. Связанный с .NET процесс с использованием COM вызывает у меня интерес, поскольку я ранее описывал в другом сообщении блога, как можно использовать COM-объекты, реализованные в .NET. Если нам повезет, что этот COM-объект реализован в .NET, мы сможем определить, реализован ли он в .NET, запросив его интерфейсы, например, мы используем команду Get-ComInterface в моем модуле OleViewDotNet PowerShell, как показано на следующем снимке экрана.
Нам не повезло, этот объект не реализован в .NET, поскольку вы, по крайней мере, ожидали увидеть экземпляр интерфейса _Object. Реализован только один интерфейс, ICorSvcBindToWorker, поэтому давайте углубимся в этот интерфейс, чтобы увидеть, есть ли что-нибудь, что мы можем эксплуатировать.
Что-то привлекло мое внимание, на снимке экрана есть столбец HasTypeLib, для ICorSvcBindToWorker мы видим, что для столбца установлено значение True.
HasTypeLib указывает, что прокси-код интерфейса не реализуется с использованием предопределенного потока байтов NDR, который он генерирует на лету из библиотеки типов. Я злоупотреблял этим механизмом автоматического создания прокси, прежде чем подняться до уровня SYSTEM, о чем сообщалось как о проблеме 1112. В этом выпуске я использовал интересное поведение системной таблицы запущенных объектов (ROT), чтобы вызвать смешение типов в системной службе COM. Несмотря на то, что Microsoft устранила проблему для User to SYSTEM, ничто не мешает нам использовать уловку путаницы типов, чтобы использовать процесс MSCORSVW, работающий как PPL с тем же уровнем привилегий, и добиться выполнения произвольного кода. Еще одно преимущество использования библиотеки типов заключается в том, что обычный прокси-сервер загружается как DLL, что означает, что он должен соответствовать требованиям уровня подписи PPL; однако библиотека типов - это просто данные, поэтому их можно загрузить в PPL без каких-либо нарушений уровня подписи.
Как работает смешение типов? Взглянем на интерфейс ICorSvcBindToWorker из библиотеки типов:
Один BindToRuntimeWorker принимает 5 параметров, 4 из которых являются входящими, а 1 — исходящими. При попытке доступа к методу через DCOM из нашего ненадежного процесса система автоматически сгенерирует прокси и заглушку для вызова. Это будет включать в себя маршалинг параметров интерфейса COM в буфер, отправку буфера удаленному процессу, а затем демаршалинг на указатель перед вызовом реальной функции. Например, представьте себе более простую функцию DoSomething, которая принимает единственный указатель Iunknown. Процесс маршалинга выглядит следующим образом:
Операция вызова метода следующая:
- Ненадежный процесс вызывает DoSomething на интерфейсе, который на самом деле является указателем на DoSomethingProxy, который был автоматически сгенерирован из библиотеки типов, передающей параметр указателя IUnknown.
- DoSomethingProxy маршалирует параметр указателя IUnknown в буфер и вызывает через RPC заглушку в защищенном процессе.
- Среда выполнения COM вызывает метод DoSomethingStub для обработки вызова. Этот метод демаршалирует указатель интерфейса из буфера. Обратите внимание, что этот указатель не является исходным указателем из шага 1, скорее всего, это новый прокси, который обращается к ненадежному процессу.
- Заглушка вызывает реальный реализованный метод внутри сервера, передавая немаршалированный указатель интерфейса.
- DoSomething использует указатель интерфейса, например, вызывая AddRef для него через VTable объекта.
Как бы мы это использовали? Все, что нам нужно сделать, это изменить библиотеку типов, чтобы вместо передачи указателя на интерфейс мы передавали почти все остальное. Пока файл библиотеки типов находится в системном месте, которое мы не можем изменить, мы можем просто заменить регистрацию для него в кусте реестра текущего пользователя или использовать тот же трюк ROT, который использовался до выпуска 1112. Например, если мы изменим библиотеку типов, чтобы передать целое число вместо указателя на интерфейс, мы получим следующее:
Теперь работа маршала изменится следующим образом:
- Ненадежный процесс вызывает DoSomething в интерфейсе, который на самом деле является указателем на DoSomethingProxy, который был автоматически сгенерирован из библиотеки типов, передающей произвольный целочисленный параметр.
- DoSomethingProxy маршалирует целочисленный параметр в буфер и вызывает через RPC заглушку в защищенном процессе.
- Среда выполнения COM вызывает метод DoSomethingStub для обработки вызова. Этот метод демаршалирует целое число из буфера.
- Заглушка вызывает реальный метод реализации внутри сервера, передавая целое число в качестве параметра. Однако DoSomething не изменился, это все тот же метод, который принимает указатель интерфейса. Поскольку на данный момент среда выполнения COM больше не имеет информации о типе, целое число перепутано с указателем интерфейса.
- DoSomething использует указатель интерфейса, например, вызывая AddRef для него через VTable объекта. Поскольку этот указатель полностью находится под контролем ненадежного процесса, это, вероятно, приведет к выполнению произвольного кода.
Изменяя тип параметра с указателя интерфейса на целое число, мы вызываем путаницу типов, которая позволяет нам разыменовать произвольный указатель, что приводит к выполнению произвольного кода. Мы могли бы даже упростить атаку, добавив в библиотеку типов следующую структуру:
Если мы передадим указатель на FakeObject вместо указателя интерфейса, автоматически сгенерированный прокси будет маршалировать структуру и ее BSTR, воссоздавая ее на другой стороне в заглушке. Поскольку BSTR - это строка с подсчетом, она может содержать значения NULL, поэтому это создаст указатель на объект, который содержит указатель на произвольный массив байтов, который может действовать как Vtable. Поместите в этот BSTR указатели на известные функции, и вы сможете легко перенаправить выполнение, не угадывая расположение подходящего буфера Vtable.
Чтобы полностью эксплуатировать это, нам нужно будет вызвать подходящий метод, возможно, запустив цепочку ROP, и нам также, возможно, придется обойти CFG. Все это звучит слишком похоже на тяжелую работу, поэтому вместо этого я воспользуюсь другим подходом к запуску произвольного кода в двоичном файле PPL, злоупотребляя KnownDll.
KnownDlls и защищенные процессы.
В моем предыдущем сообщении в блоге я описал метод повышения привилегий от уязвимости создания произвольного каталога объектов до SYSTEM путем добавления записи в каталог KnownDlls и загрузки произвольной DLL в привилегированный процесс. Я заметил, что это также был администратор внедрения кода PPL, поскольку PPL также загружает библиотеки DLL из системного расположения KnownDlls. Поскольку проверка подписи кода выполняется во время создания секции, а не сопоставления секций, до тех пор, пока вы можете поместить запись в KnownDlls, вы можете загружать что угодно в PPL, даже неподписанный код.
Это не сразу кажется таким полезным, мы не можем писать в KnownDlls, не будучи администратором, и даже тогда без некоторых хитрых приемов. Однако стоит посмотреть, как загружается известная DLL, чтобы понять, как ею можно злоупотреблять. В коде загрузчика NTDLL (LDR) есть следующая функция, позволяющая определить, существует ли ранее известная DLL.
Функция LdrpFindKnownDll вызывает NtOpenSection, чтобы открыть объект именованного раздела для известной библиотеки DLL. Он не открывает абсолютный путь, вместо этого он использует функцию собственных системных вызовов, чтобы указать корневой каталог для поиска имени объекта в структуре OBJECT_ATTRIBUTES. Этот корневой каталог берется из глобальной переменной LdrpKnownDllDirectoryHandle. Реализация вызова таким образом позволяет загрузчику указывать только имя файла (например, EXAMPLE.DLL) и не должны восстанавливать абсолютный путь, поскольку поиск выполняется относительно существующего каталога. В поисках ссылок на LdrpKnownDllDirectoryHandle мы можем обнаружить, что он инициализирован в LdrpInitializeProcess следующим образом:
Этот код не должен быть таким неожиданным, реализация вызывает NtOpenDirectoryObject, передавая абсолютный путь к каталогу KnownDlls в качестве имени объекта. Открытый дескриптор сохраняется в глобальной переменной LdrpKnownDllDirectoryHandle для дальнейшего использования. Стоит отметить, что этот код проверяет PEB, чтобы определить, является ли текущий процесс полностью защищенным. Поддержка загрузки известных DLL отключена в режиме полностью защищенного процесса, поэтому даже с правами администратора и хитрым приемом, который я описал в последнем сообщении блога, мы могли скомпрометировать только PPL, а не PP.
Как это знание нам помогает? Мы можем использовать наш трюк с путаницей типа COM, чтобы записывать значения в произвольные области памяти вместо того, чтобы пытаться перехватить выполнение кода, что приведет к атаке только данных. Поскольку мы можем унаследовать любые дескрипторы, которые нам нравятся, в новый процесс PPL, мы можем настроить каталог объектов с именованным разделом, а затем использовать путаницу типов, чтобы изменить значение LdrpKnownDllDirectoryHandle на значение унаследованного дескриптора. Если мы вызовем загрузку DLL из System32 с известным именем, LDR проверит наш поддельный каталог на предмет именованного раздела и отобразит наш неподписанный код в память, даже вызвав для нас DllMain. Нет необходимости инжектировать потоки, ROP или обходить CFG.
Все, что нам нужно, это подходящий примитив для записи произвольного значения, к сожалению, хотя я мог найти методы, которые вызывали бы произвольную запись, я не мог в достаточной степени контролировать записываемое значение. В конце я использовал следующий интерфейс и метод, которые были реализованы для объекта, возвращенного ICorSvcBindToWorker :: BindToRuntimeWorker.
interface ICorSvcPooledWorker : IUnknown {
HRESULT CanReuseProcess(
[in] OptimizationScenario scenario,
[in] ICorSvcLogger* pCorSvcLogger,
[out] long* pCanContinue);
};
В реализации CanReuseProcess целевое значение pCanContinue всегда инициализируется значением 0. Поэтому, заменив [out] long * в определении библиотеки типов на [in] long, мы можем получить 0, записанный в любую указанную нами ячейку памяти. Предварительно заполнив нижние 16 бит таблицы дескрипторов нового процесса дескрипторами поддельного каталога KnownDlls, мы можем быть уверены в наличии псевдонима между реальными KnownDll, которые будут открываться после запуска процесса, и нашими поддельными, просто изменив верхние 16 бит дескриптора на 0. Это показано на следующей диаграмме:
После того, как мы перезаписали верхние 16 бит на 0 (запись - 32 бита, но дескрипторы - 64 бита в 64-битном режиме, поэтому мы не перезаписываем ничего важного), LdrpKnownDllDirectoryHandle теперь указывает на один из наших поддельных дескрипторов KnownDll. Затем мы можем легко вызвать загрузку DLL, отправив настраиваемый маршалированный объект в тот же метод, и мы получим выполнение произвольного кода внутри PPL.
Повышение до PPL-Windows TCB
На этом мы не можем остановиться, атака MSCORSVW дает нам PPL только на уровне подписи CodeGen, а не Windows TCB. Зная, что создание поддельной кэшированной подписанной DLL должно выполняться в PPL, а также о том, что Microsoft оставляет бэкдор для процессов PPL на любом уровне подписи, я преобразовал свой код C# из проблемы 1332 в C++ для создания поддельной кэшированной подписанной DLL. Злоупотребляя перехватом DLL в WERFAULTSECURE.EXE, который будет работать как PPL Windows TCB, мы должны добиться выполнения кода на желаемом уровне подписи. Это работало в Windows 10 1709 и ранее, но не работало в 1803. Очевидно, что Microsoft каким-то образом изменила поведение уровня подписи в кэше, возможно, они полностью отказались от доверия к PPL. Это казалось маловероятным, так как могло бы отрицательно сказаться на производительности.
После небольшого обсуждения этого вопроса с Алексом Ионеску я решил собрать быстрый парсер с информацией от Алекса для кэшированных данных подписи в файле. Он отображается в NtObjectManager как команда Get-NtCachedSigningLevel. Я запустил эту команду против поддельного подписанного двоичного файла и системного двоичного файла, который также был кэширован подписанным, и сразу заметил разницу:
Для фальшивого подписанного файла для флагов установлено значение TrustedSignature (0x02), однако для системного двоичного файла PowerShell не может декодировать перечисление и просто выводит целочисленное значение 66, которое равно 0x42 в шестнадцатеричном формате. Значение 0x40 было дополнительным флагом поверх исходного флага доверенной подписи. Казалось вероятным, что без установки этого флага DLL не будет загружена в процесс PPL. Что-то должно устанавливать этот флаг, поэтому я решил проверить, что произойдет, если я загрузил действительную кэшированную подписанную DLL без дополнительного флага в процесс PPL. Наблюдая за ним в Process Monitor, я получил свой ответ:
Трассировка Process Monitor показывает, что сначала ядро запрашивает расширенные атрибуты (EA) из библиотеки DLL. Кэшированные данные уровня подписи хранятся в эксперте файла, так что это почти наверняка показатель чтения кэшированного уровня подписи. В полной трассировке показаны артефакты проверки полной подписи, такие как перечисление файлов каталога. Я удалил эти артефакты со снимка экрана для краткости.
Наконец, EA установлен, и если я проверю уровень подписи файла в кэше, он теперь включает дополнительный флаг. Таким образом, установка уровня кэширования подписи выполняется автоматически, вопрос в том, как? Вытащив трассировку стека, мы можем увидеть, как это происходит:
Посмотрев на середину трассировки стека, мы видим, что вызов CipSetFileCache происходит от вызова NtCreateSection. Ядро автоматически кэширует подпись, когда это имеет смысл, например в PPL, чтобы при последующем сопоставлении образов не нужно было повторно проверять подпись. Можно сопоставить раздел образа из файла с доступом на запись, чтобы мы могли повторно использовать ту же атаку из проблемы 1332 и заменить вызов NtSetCachedSigningLevel на NtCreateSection, и мы могли подделать подпись любой DLL. Оказалось, что вызов для установки файлового кеша произошел после проверки записи, введенной для исправления ошибки 1332, и поэтому можно было использовать это, чтобы снова обойти Device Guard. По этой причине я сообщил об обходе как об ошибке 1597, которая была исправлена в сентябре 2018 года как CVE-2018-8449. Однако, как и в случае с ошибкой 1332, бэкдор для PPL все еще существует, поэтому, хотя исправление устранило обход Device Guard, его все еще можно использовать для перехода от PPL-CodeGen к PPL-WindowsTCB.
Заключение
В этом блоге показано, как мне удалось внедрить произвольный код в PPL, не требуя прав администратора. Что вы могли бы сделать с этой вновь обретенной силой? На самом деле это не так уж важно для обычного пользователя, но есть некоторые части ОС, такие как Магазин Windows, которые полагаются на PPL для защиты файлов и ресурсов, которые вы не можете изменить как обычный пользователь.Если вы повысите уровень до администратора, а затем внедрите в PPL, вы получите гораздо больше возможностей для атак, таких как CSRSS (с помощью которого вы, безусловно, можете получить выполнение кода ядра) или атаковать Защитник Windows, который работает как PPL Anti-Malware. Я уверен, что со временем большинство вариантов использования PPL будут заменены приложениями в виртуальном безопасном режиме (VSM) и изолированном режиме пользователя (IUM), которые имеют более высокие гарантии безопасности и также считаются границами безопасности, которые Microsoft будет защищать и исправлять.
Сообщал ли я об этих проблемах в Microsoft? Microsoft дала понять, что они не будут исправлять проблемы, затрагивающие только PP и PPL, в бюллетене по безопасности. Без бюллетеня по безопасности исследователь не получает подтверждения о находке, например CVE. Проблема не будет исправлена в текущих версиях Windows, хотя может быть исправлена в следующей основной версии. Ранее подтверждение политики Microsoft по устранению конкретной проблемы безопасности было основано на прецеденте, однако недавно они опубликовали список технологий Windows, которые будут или не будут исправлены в критериях службы безопасности Windows, которые, как показано ниже для Protected Process Light, Microsoft не будет исправлять проблемы, связанные с этой функцией, и платить за них. Поэтому с этого момента я не буду связываться с Microsoft, если обнаружу проблемы, которые, по моему мнению, влияют только на PP или PPL.
Единственная ошибка, о которой я сообщил в Microsoft, была исправлена только потому, что ее можно было использовать для обхода Device Guard. Если подумать, то только исправление для Device Guard несколько странно. Я все еще могу обойти Device Guard, внедрив в PPL и установив кэшированный уровень подписи, и все же Microsoft не исправит проблемы PPL, но исправит проблемы Device Guard. Документ "Критерии службы безопасности Windows" во многом помогает прояснить, что Microsoft будет и что не будет исправлять. Безопасная функция редко бывает изолированной, функция почти наверняка безопасна, потому что другие функции позволяют это сделать.
Во второй части этого блога мы расскажем, как мне удалось взломать процессы Full PP-WindowsTCB, используя еще одну интересную функцию COM.
Источник: https://googleprojectzero.blogspot.com/2018/10/injecting-code-into-windows-protected.html
Автор перевода: yashechka
Переведено специально для https://xss.pro
В этой статье я собираюсь описать процесс, через который я прошел, чтобы обнаружить способ внедрения кода в PPL в Windows 10 1803. Поскольку единственная проблема, которую Microsoft считала нарушающей защищенные границы безопасности, теперь решена, я могу обсудить эксплойт более подробно.
Справочная информация о защищенных процессах Windows
Истоки модели защищенного процесса Windows (PP) уходят корнями в Vista, где она была введена для защиты процессов DRM. Модель защищенного процесса была сильно ограничена, ограничивая загруженные библиотеки DLL подмножеством кода, установленного с операционной системой. Также, чтобы исполняемый файл считался подходящим для запуска, он должен быть подписан с помощью специального сертификата Microsoft, встроенного в двоичный файл. Одна защита, которую обеспечивает ядро, заключается в том, что незащищенный процесс не может открыть дескриптор защищенного процесса с достаточными правами для ввода произвольного кода или чтения памяти.
В Windows 8.1 был представлен новый механизм Protected Process Light (PPL), который сделал защиту более универсальной. PPL ослабил некоторые ограничения на то, какие библиотеки DLL считались допустимыми для загрузки в защищенный процесс, и ввел различные требования к подписи для основного исполняемого файла. Еще одним большим изменением стало введение набора уровней подписи для разделения различных типов защищенных процессов. PPL на одном уровне может открыть для полного доступа любой процесс на том же уровне подписи или ниже, с ограниченным набором доступа, предоставленным уровням выше. Эти уровни подписи были распространены на старую модель PP, PP на одном уровне может открывать все PP и PPL на том же уровне подписи или ниже, однако обратное не верно, PPL никогда не может открывать PP на любом уровне подписи для полного доступ. Некоторые из уровней и эти отношения показаны ниже:
Уровни подписи позволяют Microsoft открывать защищенные процессы для третьих сторон, хотя в настоящее время единственный тип защищенного процесса, который может создать третье лицо, - это PPL Anti-Malware. Уровень защиты от вредоносных программ является особым, поскольку он позволяет третьей стороне добавлять дополнительные разрешенные ключи подписи путем регистрации сертификата раннего запуска защиты от вредоносных программ (ELAM). Существует также TruePlay от Microsoft, технология Anti-Cheat для игр, использующая компоненты PPL, но это не очень важно для этого обсуждения.
Я мог бы потратить большую часть этого сообщения в блоге на описание того, как PP и PPL работают под капотом, но я рекомендую вместо этого прочитать серию сообщений в блоге Алекса Ионеску, которые помогут лучше разорабраться. Хотя сообщения в блоге в основном основаны на Windows 8.1, большинство концепций в Windows 10 существенно не изменились.
Я уже писал о защищенных процессах до [https://googleprojectzero.blogspot.com/2017/08/bypassing-virtualbox-process-hardening.html] в виде специальной реализации Oracle в их платформе виртуализации VirtualBox в Windows. В блоге показано, как я обошел защиту процесса, используя несколько различных методов. То, о чем я не упомянул в то время, было первой описанной мною техникой, заключающейся в внедрении кода JScript в процесс, которая также работала против реализации PPL от Microsoft. Я сообщил, что могу внедрить произвольный код в PPL для Microsoft (см. проблему 1336) из предосторожности на случай, если Microsoft захочет исправить это. В этом случае Microsoft решила, что это не будет исправлено как бюллетень безопасности. Однако Microsoft исправила проблему в следующем крупном выпуске Windows (версия 1803), добавив следующий код в CI.DLL, библиотеку целостности кода ядра:
C:
UNICODE_STRING g_BlockedDllsForPPL[] = {
DECLARE_USTR("scrobj.dll"),
DECLARE_USTR("scrrun.dll"),
DECLARE_USTR("jscript.dll"),
DECLARE_USTR("jscript9.dll"),
DECLARE_USTR("vbscript.dll")
};
NTSTATUS CipMitigatePPLBypassThroughInterpreters(PEPROCESS Process,
LPBYTE Image,
SIZE_T ImageSize) {
if (!PsIsProtectedProcess(Process))
return STATUS_SUCCESS;
UNICODE_STRING OriginalImageName;
// Get the original filename from the image resources.
SIPolicyGetOriginalFilenameAndVersionFromImageBase(
Image, ImageSize, &OriginalImageName);
for(int i = 0; i < _countof(g_BlockedDllsForPPL); ++i) {
if (RtlEqualUnicodeString(g_BlockedDllsForPPL[i],
&OriginalImageName, TRUE)) {
return STATUS_DYNAMIC_CODE_BLOCKED;
}
}
return STATUS_SUCCESS;
}
Исправление проверяет исходное имя файла в разделе ресурсов загружаемого образу по черному списку из 5 DLL. Черный список включает библиотеки DLL, такие как JSCRIPT.DLL, который реализует исходный механизм сценариев JScript, и SCROBJ.DLL, который реализует объекты скриптлетов. Если ядро обнаруживает, что PP или PPL загружает одну из этих DLL, загрузка изображения отклоняется с помощью STATUS_DYNAMIC_CODE_BLOCKED. Это убивает мой эксплойт, если вы измените раздел ресурсов одной из перечисленных DLL, подпись изображения станет недействительной, что приведет к сбою загрузки изображения из-за несоответствия криптографического хэша. Фактически это то же самое исправление, которое Oracle использовала для блокировки атаки в VirtualBox, хотя оно было реализовано в пользовательском режиме.
Поиск новых целей
Предыдущий метод внедрения с использованием кода сценария был общим методом, который работал с любым PPL, загружающим COM-объект. С исправленной техникой я решил вернуться и посмотреть, какие исполняемые файлы будут загружаться как PPL, чтобы увидеть, есть ли в них какие-либо очевидные уязвимости, которые я мог бы использовать для выполнения произвольного кода. Я мог бы выбрать полный PP, но PPL казался более простым из двух, и мне нужно с чего-то начать. Существует так много способов внедрения в PPL, если бы мы могли просто получить права администратора, наименьший из которых - это просто загрузка драйвера ядра. По этой причине любая обнаруженная мной уязвимость должна работать с учетной записью обычного пользователя. Также я хотел получить наивысший уровень подписи, который я мог получить, что означает PPL на уровне подписи Windows TCB.
Первым шагом было определение исполняемых файлов, которые запускаются как защищенный процесс, что дает нам максимальную поверхность атаки для анализа уязвимостей. Судя по сообщениям в блоге от Алекса, казалось, что для загрузки как PP или PPL сертификат подписи требует специального идентификатора объекта (OID) в расширении расширенного использования ключа (EKU) сертификата. Есть отдельные OID для PP и PPL; мы можем увидеть это ниже, сравнив WERFAULTSECURE.EXE, который может работать как PP/ PPL, и CSRSS.EXE, который может работать только как PPL.
Я решил поискать исполняемые файлы, которые имеют встроенную подпись с этими OID EKU, и это даст мне список всех исполняемых файлов для поиска уязвимого поведения. Я написал командлет Get-EmbeddedAuthenticodeSignature для моего модуля NtObjectManager PowerShell для извлечения этой информации.
На этом этапе я понял, что существует проблема с подходом к использованию сертификата подписи, я ожидал, что есть много двоичных файлов, которым будет разрешено работать как PP или PPL, которые отсутствовали в списке, который я создал. Поскольку PP изначально был разработан для DRM, не было очевидного исполняемого файла для обработки пути защищенного носителя, такого как AUDIODG.EXE. Кроме того, основываясь на моем предыдущем исследовании Device Guard и Windows 10S, я знал, что в платформе .NET должен быть исполняемый файл, который мог бы работать как PPL для добавления кэшированной информации об уровне подписи в сгенерированные NGEN двоичные файлы (NGEN - это Ahead-of-Time JIT для преобразования сборки .NET в собственный код). Критерии PP/PPL оказались более гибкими, чем я ожидал. Вместо статического анализа я решил выполнить динамический анализ, просто для начала защищал каждый исполняемый файл, который я мог перечислить, и запрашивал предоставленный уровень защиты. Я написал следующий сценарий для тестирования одного исполняемого файла:
C:
Import-Module NtObjectManager
function Test-ProtectedProcess {
[CmdletBinding()]
param(
[Parameter(Mandatory, ValueFromPipelineByPropertyName)]
[string]$FullName,
[NtApiDotNet.PsProtectedType]$ProtectedType = 0,
[NtApiDotNet.PsProtectedSigner]$ProtectedSigner = 0
)
BEGIN {
$config = New-NtProcessConfig abc -ProcessFlags ProtectedProcess `
-ThreadFlags Suspended -TerminateOnDispose `
-ProtectedType $ProtectedType `
-ProtectedSigner $ProtectedSigner
}
PROCESS {
$path = Get-NtFilePath $FullName
Write-Host $path
try {
Use-NtObject($p = New-NtProcess $path -Config $config) {
$prot = $p.Process.Protection
$props = @{
Path=$path;
Type=$prot.Type;
Signer=$prot.Signer;
Level=$prot.Level.ToString("X");
}
$obj = New-Object –TypeName PSObject –Prop $props
Write-Output $obj
}
} catch {
}
}
}
При выполнении этого сценария определяется функция Test-ProtectedProcess. Функция берет путь к исполняемому файлу, запускает этот исполняемый файл с указанным уровнем защиты и проверяет, был ли он успешным. Если параметры ProtectedType и ProtectedSigner равны 0, тогда ядро определяет "лучший" уровень процесса. Это приводит к некоторым неприятным особенностям, например, SVCHOST.EXE явно помечен как PPL и будет работать на уровне PPL-Windows, однако, поскольку это также подписанный компонент ОС, ядро определит, что его максимальный уровень – PP-Authenticode. Еще одна интересная особенность заключается в том, что использование собственных API-интерфейсов создания процессов позволяет запускать DLL в качестве основного исполняемого образа. Поскольку значительное количество системных библиотек DLL имеют встроенные подписи Microsoft, их также можно запускать как PP-Authenticode, хотя это не обязательно так полезно. Список двоичных файлов, которые будут работать на PPL, показан ниже вместе с их максимальным уровнем подписи.
| Path | Signing Level |
| C:\windows\Microsoft.Net\Framework\v4.0.30319\mscorsvw.exe | CodeGen |
| C:\windows\Microsoft.Net\Framework64\v4.0.30319\mscorsvw.exe | CodeGen |
| C:\windows\system32\SecurityHealthService.exe | Windows |
| C:\windows\system32\svchost.exe | Windows |
| C:\windows\system32\xbgmsvc.exe | Windows |
| C:\windows\system32\csrss.exe | Windows TCB |
| C:\windows\system32\services.exe | Windows TCB |
| C:\windows\system32\smss.exe | Windows TCB |
| C:\windows\system32\werfaultsecure.exe | Windows TCB |
| C:\windows\system32\wininit.exe | Windows TCB |
Внедрение произвольного кода в NGEN
Внимательно просмотрев список исполняемых файлов, работающих как PPL, я остановился на попытке атаковать ранее упомянутый двоичный файл .NET NGEN, MSCORSVW.EXE. Мое объяснение выбора бинарного файла NGEN было:
- Большинство других двоичных файлов представляют собой служебные двоичные файлы, которым для правильного запуска могут потребоваться права администратора.
- Бинарный файл, вероятно, будет загружать сложные функции, такие как .NET framework, а также иметь несколько взаимодействий COM (моя технология для странного поведения).
- В худшем случае это все равно может привести к обходу Device Guard, поскольку причина, по которой он работает как PPL, заключается в том, чтобы предоставить ему доступ к API ядра для применения кэшированного уровня подписи. Любая ошибка в работе этого двоичного файла может быть использована, даже если мы не можем запустить произвольный код в PPL.
Но есть проблема с двоичным файлом NGEN, в частности, он не соответствует моим критериям, согласно которым я получаю высший уровень подписи, Windows TCB. Однако я знал, что, когда Microsoft исправила проблему 1332, они ушли с черного хода, где во время процесса подписи можно было сохранить доступный для записи дескриптор, если вызывающим процессом является PPL, как показано ниже:
C:
NTSTATUS CiSetFileCache(HANDLE Handle, ...) {
PFILE_OBJECT FileObject;
ObReferenceObjectByHandle(Handle, &FileObject);
if (FileObject->SharedWrite ||
(FileObject->WriteAccess &&
PsGetProcessProtection().Type != PROTECTED_LIGHT)) {
return STATUS_SHARING_VIOLATION;
}
// Continue setting file cache.
}
Если бы я мог получить выполнение кода внутри двоичного файла NGEN, я мог бы повторно использовать этот бэкдор для кеширования подписи произвольного файла, который будет загружаться в любой PPL. Тогда я мог бы перехватить DLL полный процесс PPL-WindowsTCB, чтобы достичь своей цели.
Чтобы начать расследование, нам нужно определить, как использовать исполняемый файл MSCORSVW. Использование MSCORSVW нигде не задокументировано Microsoft, поэтому нам придется немного покопаться. Во-первых, этот двоичный файл не должен запускаться напрямую, вместо этого он вызывается NGEN при создании двоичного файла NGEN. Таким образом, мы можем запустить двоичный файл NGEN и использовать такой инструмент, как Process Monitor, чтобы узнать, какая командная строка используется для процесса MSCORSVW. Выполнение команды:
C:\> NGEN install c:\some\binary.dll
Результат приводит к выполнению следующей командной строки:
MSCORSVW -StartupEvent A -InterruptEvent B -NGENProcess C -Pipe D
A, B, C и D - это дескрипторы, которые NGEN гарантирует, что они будут унаследованы в новый процесс перед его запуском. Поскольку мы не видим никаких исходных параметров командной строки NGEN, похоже, что они передаются через механизм IPC. Параметр "Pipe" указывает на то, что именованные каналы используются для IPC. Копаясь в коде MSCORSVW, мы находим метод NGenWorkerEmbedding, который выглядит следующим образом:
C++:
void NGenWorkerEmbedding(HANDLE hPipe) {
CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED);
CorSvcBindToWorkerClassFactory factory;
// Marshal class factory.
IStream* pStm;
CreateStreamOnHGlobal(nullptr, TRUE, &pStm);
CoMarshalInterface(pStm, &IID_IClassFactory, &factory,
MSHCTX_LOCAL, nullptr, MSHLFLAGS_NORMAL);
// Read marshaled object and write to pipe.
DWORD length;
char* buffer = ReadEntireIStream(pStm, &length);
WriteFile(hPipe, &length, sizeof(length));
WriteFile(hPipe, buffer, length);
CloseHandle(hPipe);
// Set event to synchronize with parent.
SetEvent(hStartupEvent);
// Pump message loop to handle COM calls.
MessageLoop();
// ...
}
Этот код не совсем то, что я ожидал. Вместо использования именованного канала для всего канала связи он используется только для передачи маршалированного COM-объекта обратно вызывающему процессу. COM-объект - это экземпляр фабрики классов, обычно вы регистрируете фабрику с помощью CoRegisterClassObject, но это сделает его доступным для всех процессов с одинаковым уровнем безопасности, поэтому вместо этого с помощью маршалинга соединение можно оставить частным только для двоичного файла NGEN, который породил MSCORSVW. Связанный с .NET процесс с использованием COM вызывает у меня интерес, поскольку я ранее описывал в другом сообщении блога, как можно использовать COM-объекты, реализованные в .NET. Если нам повезет, что этот COM-объект реализован в .NET, мы сможем определить, реализован ли он в .NET, запросив его интерфейсы, например, мы используем команду Get-ComInterface в моем модуле OleViewDotNet PowerShell, как показано на следующем снимке экрана.
Нам не повезло, этот объект не реализован в .NET, поскольку вы, по крайней мере, ожидали увидеть экземпляр интерфейса _Object. Реализован только один интерфейс, ICorSvcBindToWorker, поэтому давайте углубимся в этот интерфейс, чтобы увидеть, есть ли что-нибудь, что мы можем эксплуатировать.
Что-то привлекло мое внимание, на снимке экрана есть столбец HasTypeLib, для ICorSvcBindToWorker мы видим, что для столбца установлено значение True.
HasTypeLib указывает, что прокси-код интерфейса не реализуется с использованием предопределенного потока байтов NDR, который он генерирует на лету из библиотеки типов. Я злоупотреблял этим механизмом автоматического создания прокси, прежде чем подняться до уровня SYSTEM, о чем сообщалось как о проблеме 1112. В этом выпуске я использовал интересное поведение системной таблицы запущенных объектов (ROT), чтобы вызвать смешение типов в системной службе COM. Несмотря на то, что Microsoft устранила проблему для User to SYSTEM, ничто не мешает нам использовать уловку путаницы типов, чтобы использовать процесс MSCORSVW, работающий как PPL с тем же уровнем привилегий, и добиться выполнения произвольного кода. Еще одно преимущество использования библиотеки типов заключается в том, что обычный прокси-сервер загружается как DLL, что означает, что он должен соответствовать требованиям уровня подписи PPL; однако библиотека типов - это просто данные, поэтому их можно загрузить в PPL без каких-либо нарушений уровня подписи.
Как работает смешение типов? Взглянем на интерфейс ICorSvcBindToWorker из библиотеки типов:
interface ICorSvcBindToWorker : IUnknown {
HRESULT BindToRuntimeWorker(
[in] BSTR pRuntimeVersion,
[in] unsigned long ParentProcessID,
[in] BSTR pInterruptEventName,
[in] ICorSvcLogger* pCorSvcLogger,
[out] ICorSvcWorker** pCorSvcWorker);
};
Один BindToRuntimeWorker принимает 5 параметров, 4 из которых являются входящими, а 1 — исходящими. При попытке доступа к методу через DCOM из нашего ненадежного процесса система автоматически сгенерирует прокси и заглушку для вызова. Это будет включать в себя маршалинг параметров интерфейса COM в буфер, отправку буфера удаленному процессу, а затем демаршалинг на указатель перед вызовом реальной функции. Например, представьте себе более простую функцию DoSomething, которая принимает единственный указатель Iunknown. Процесс маршалинга выглядит следующим образом:
Операция вызова метода следующая:
- Ненадежный процесс вызывает DoSomething на интерфейсе, который на самом деле является указателем на DoSomethingProxy, который был автоматически сгенерирован из библиотеки типов, передающей параметр указателя IUnknown.
- DoSomethingProxy маршалирует параметр указателя IUnknown в буфер и вызывает через RPC заглушку в защищенном процессе.
- Среда выполнения COM вызывает метод DoSomethingStub для обработки вызова. Этот метод демаршалирует указатель интерфейса из буфера. Обратите внимание, что этот указатель не является исходным указателем из шага 1, скорее всего, это новый прокси, который обращается к ненадежному процессу.
- Заглушка вызывает реальный реализованный метод внутри сервера, передавая немаршалированный указатель интерфейса.
- DoSomething использует указатель интерфейса, например, вызывая AddRef для него через VTable объекта.
Как бы мы это использовали? Все, что нам нужно сделать, это изменить библиотеку типов, чтобы вместо передачи указателя на интерфейс мы передавали почти все остальное. Пока файл библиотеки типов находится в системном месте, которое мы не можем изменить, мы можем просто заменить регистрацию для него в кусте реестра текущего пользователя или использовать тот же трюк ROT, который использовался до выпуска 1112. Например, если мы изменим библиотеку типов, чтобы передать целое число вместо указателя на интерфейс, мы получим следующее:
Теперь работа маршала изменится следующим образом:
- Ненадежный процесс вызывает DoSomething в интерфейсе, который на самом деле является указателем на DoSomethingProxy, который был автоматически сгенерирован из библиотеки типов, передающей произвольный целочисленный параметр.
- DoSomethingProxy маршалирует целочисленный параметр в буфер и вызывает через RPC заглушку в защищенном процессе.
- Среда выполнения COM вызывает метод DoSomethingStub для обработки вызова. Этот метод демаршалирует целое число из буфера.
- Заглушка вызывает реальный метод реализации внутри сервера, передавая целое число в качестве параметра. Однако DoSomething не изменился, это все тот же метод, который принимает указатель интерфейса. Поскольку на данный момент среда выполнения COM больше не имеет информации о типе, целое число перепутано с указателем интерфейса.
- DoSomething использует указатель интерфейса, например, вызывая AddRef для него через VTable объекта. Поскольку этот указатель полностью находится под контролем ненадежного процесса, это, вероятно, приведет к выполнению произвольного кода.
Изменяя тип параметра с указателя интерфейса на целое число, мы вызываем путаницу типов, которая позволяет нам разыменовать произвольный указатель, что приводит к выполнению произвольного кода. Мы могли бы даже упростить атаку, добавив в библиотеку типов следующую структуру:
struct FakeObject {
BSTR FakeVTable;
};
Если мы передадим указатель на FakeObject вместо указателя интерфейса, автоматически сгенерированный прокси будет маршалировать структуру и ее BSTR, воссоздавая ее на другой стороне в заглушке. Поскольку BSTR - это строка с подсчетом, она может содержать значения NULL, поэтому это создаст указатель на объект, который содержит указатель на произвольный массив байтов, который может действовать как Vtable. Поместите в этот BSTR указатели на известные функции, и вы сможете легко перенаправить выполнение, не угадывая расположение подходящего буфера Vtable.
Чтобы полностью эксплуатировать это, нам нужно будет вызвать подходящий метод, возможно, запустив цепочку ROP, и нам также, возможно, придется обойти CFG. Все это звучит слишком похоже на тяжелую работу, поэтому вместо этого я воспользуюсь другим подходом к запуску произвольного кода в двоичном файле PPL, злоупотребляя KnownDll.
KnownDlls и защищенные процессы.
В моем предыдущем сообщении в блоге я описал метод повышения привилегий от уязвимости создания произвольного каталога объектов до SYSTEM путем добавления записи в каталог KnownDlls и загрузки произвольной DLL в привилегированный процесс. Я заметил, что это также был администратор внедрения кода PPL, поскольку PPL также загружает библиотеки DLL из системного расположения KnownDlls. Поскольку проверка подписи кода выполняется во время создания секции, а не сопоставления секций, до тех пор, пока вы можете поместить запись в KnownDlls, вы можете загружать что угодно в PPL, даже неподписанный код.
Это не сразу кажется таким полезным, мы не можем писать в KnownDlls, не будучи администратором, и даже тогда без некоторых хитрых приемов. Однако стоит посмотреть, как загружается известная DLL, чтобы понять, как ею можно злоупотреблять. В коде загрузчика NTDLL (LDR) есть следующая функция, позволяющая определить, существует ли ранее известная DLL.
C:
NTSTATUS LdrpFindKnownDll(PUNICODE_STRING DllName, HANDLE *SectionHandle) {
// If KnownDll directory handle not open then return error.
if (!LdrpKnownDllDirectoryHandle)
return STATUS_DLL_NOT_FOUND;
OBJECT_ATTRIBUTES ObjectAttributes;
InitializeObjectAttributes(&ObjectAttributes,
&DllName,
OBJ_CASE_INSENSITIVE,
LdrpKnownDllDirectoryHandle,
nullptr);
return NtOpenSection(SectionHandle,
SECTION_ALL_ACCESS,
&ObjectAttributes);
}
Функция LdrpFindKnownDll вызывает NtOpenSection, чтобы открыть объект именованного раздела для известной библиотеки DLL. Он не открывает абсолютный путь, вместо этого он использует функцию собственных системных вызовов, чтобы указать корневой каталог для поиска имени объекта в структуре OBJECT_ATTRIBUTES. Этот корневой каталог берется из глобальной переменной LdrpKnownDllDirectoryHandle. Реализация вызова таким образом позволяет загрузчику указывать только имя файла (например, EXAMPLE.DLL) и не должны восстанавливать абсолютный путь, поскольку поиск выполняется относительно существующего каталога. В поисках ссылок на LdrpKnownDllDirectoryHandle мы можем обнаружить, что он инициализирован в LdrpInitializeProcess следующим образом:
C:
NTSTATUS LdrpInitializeProcess() {
// ...
PPEB peb = // ...
// If a full protected process don't use KnownDlls.
if (peb->IsProtectedProcess && !peb->IsProtectedProcessLight) {
LdrpKnownDllDirectoryHandle = nullptr;
} else {
OBJECT_ATTRIBUTES ObjectAttributes;
UNICODE_STRING DirName;
RtlInitUnicodeString(&DirName, L"\\KnownDlls");
InitializeObjectAttributes(&ObjectAttributes,
&DirName,
OBJ_CASE_INSENSITIVE,
nullptr, nullptr);
// Open KnownDlls directory.
NtOpenDirectoryObject(&LdrpKnownDllDirectoryHandle,
DIRECTORY_QUERY | DIRECTORY_TRAVERSE,
&ObjectAttributes);
}
Этот код не должен быть таким неожиданным, реализация вызывает NtOpenDirectoryObject, передавая абсолютный путь к каталогу KnownDlls в качестве имени объекта. Открытый дескриптор сохраняется в глобальной переменной LdrpKnownDllDirectoryHandle для дальнейшего использования. Стоит отметить, что этот код проверяет PEB, чтобы определить, является ли текущий процесс полностью защищенным. Поддержка загрузки известных DLL отключена в режиме полностью защищенного процесса, поэтому даже с правами администратора и хитрым приемом, который я описал в последнем сообщении блога, мы могли скомпрометировать только PPL, а не PP.
Как это знание нам помогает? Мы можем использовать наш трюк с путаницей типа COM, чтобы записывать значения в произвольные области памяти вместо того, чтобы пытаться перехватить выполнение кода, что приведет к атаке только данных. Поскольку мы можем унаследовать любые дескрипторы, которые нам нравятся, в новый процесс PPL, мы можем настроить каталог объектов с именованным разделом, а затем использовать путаницу типов, чтобы изменить значение LdrpKnownDllDirectoryHandle на значение унаследованного дескриптора. Если мы вызовем загрузку DLL из System32 с известным именем, LDR проверит наш поддельный каталог на предмет именованного раздела и отобразит наш неподписанный код в память, даже вызвав для нас DllMain. Нет необходимости инжектировать потоки, ROP или обходить CFG.
Все, что нам нужно, это подходящий примитив для записи произвольного значения, к сожалению, хотя я мог найти методы, которые вызывали бы произвольную запись, я не мог в достаточной степени контролировать записываемое значение. В конце я использовал следующий интерфейс и метод, которые были реализованы для объекта, возвращенного ICorSvcBindToWorker :: BindToRuntimeWorker.
interface ICorSvcPooledWorker : IUnknown {
HRESULT CanReuseProcess(
[in] OptimizationScenario scenario,
[in] ICorSvcLogger* pCorSvcLogger,
[out] long* pCanContinue);
};
В реализации CanReuseProcess целевое значение pCanContinue всегда инициализируется значением 0. Поэтому, заменив [out] long * в определении библиотеки типов на [in] long, мы можем получить 0, записанный в любую указанную нами ячейку памяти. Предварительно заполнив нижние 16 бит таблицы дескрипторов нового процесса дескрипторами поддельного каталога KnownDlls, мы можем быть уверены в наличии псевдонима между реальными KnownDll, которые будут открываться после запуска процесса, и нашими поддельными, просто изменив верхние 16 бит дескриптора на 0. Это показано на следующей диаграмме:
После того, как мы перезаписали верхние 16 бит на 0 (запись - 32 бита, но дескрипторы - 64 бита в 64-битном режиме, поэтому мы не перезаписываем ничего важного), LdrpKnownDllDirectoryHandle теперь указывает на один из наших поддельных дескрипторов KnownDll. Затем мы можем легко вызвать загрузку DLL, отправив настраиваемый маршалированный объект в тот же метод, и мы получим выполнение произвольного кода внутри PPL.
Повышение до PPL-Windows TCB
На этом мы не можем остановиться, атака MSCORSVW дает нам PPL только на уровне подписи CodeGen, а не Windows TCB. Зная, что создание поддельной кэшированной подписанной DLL должно выполняться в PPL, а также о том, что Microsoft оставляет бэкдор для процессов PPL на любом уровне подписи, я преобразовал свой код C# из проблемы 1332 в C++ для создания поддельной кэшированной подписанной DLL. Злоупотребляя перехватом DLL в WERFAULTSECURE.EXE, который будет работать как PPL Windows TCB, мы должны добиться выполнения кода на желаемом уровне подписи. Это работало в Windows 10 1709 и ранее, но не работало в 1803. Очевидно, что Microsoft каким-то образом изменила поведение уровня подписи в кэше, возможно, они полностью отказались от доверия к PPL. Это казалось маловероятным, так как могло бы отрицательно сказаться на производительности.
После небольшого обсуждения этого вопроса с Алексом Ионеску я решил собрать быстрый парсер с информацией от Алекса для кэшированных данных подписи в файле. Он отображается в NtObjectManager как команда Get-NtCachedSigningLevel. Я запустил эту команду против поддельного подписанного двоичного файла и системного двоичного файла, который также был кэширован подписанным, и сразу заметил разницу:
Для фальшивого подписанного файла для флагов установлено значение TrustedSignature (0x02), однако для системного двоичного файла PowerShell не может декодировать перечисление и просто выводит целочисленное значение 66, которое равно 0x42 в шестнадцатеричном формате. Значение 0x40 было дополнительным флагом поверх исходного флага доверенной подписи. Казалось вероятным, что без установки этого флага DLL не будет загружена в процесс PPL. Что-то должно устанавливать этот флаг, поэтому я решил проверить, что произойдет, если я загрузил действительную кэшированную подписанную DLL без дополнительного флага в процесс PPL. Наблюдая за ним в Process Monitor, я получил свой ответ:
Трассировка Process Monitor показывает, что сначала ядро запрашивает расширенные атрибуты (EA) из библиотеки DLL. Кэшированные данные уровня подписи хранятся в эксперте файла, так что это почти наверняка показатель чтения кэшированного уровня подписи. В полной трассировке показаны артефакты проверки полной подписи, такие как перечисление файлов каталога. Я удалил эти артефакты со снимка экрана для краткости.
Наконец, EA установлен, и если я проверю уровень подписи файла в кэше, он теперь включает дополнительный флаг. Таким образом, установка уровня кэширования подписи выполняется автоматически, вопрос в том, как? Вытащив трассировку стека, мы можем увидеть, как это происходит:
Посмотрев на середину трассировки стека, мы видим, что вызов CipSetFileCache происходит от вызова NtCreateSection. Ядро автоматически кэширует подпись, когда это имеет смысл, например в PPL, чтобы при последующем сопоставлении образов не нужно было повторно проверять подпись. Можно сопоставить раздел образа из файла с доступом на запись, чтобы мы могли повторно использовать ту же атаку из проблемы 1332 и заменить вызов NtSetCachedSigningLevel на NtCreateSection, и мы могли подделать подпись любой DLL. Оказалось, что вызов для установки файлового кеша произошел после проверки записи, введенной для исправления ошибки 1332, и поэтому можно было использовать это, чтобы снова обойти Device Guard. По этой причине я сообщил об обходе как об ошибке 1597, которая была исправлена в сентябре 2018 года как CVE-2018-8449. Однако, как и в случае с ошибкой 1332, бэкдор для PPL все еще существует, поэтому, хотя исправление устранило обход Device Guard, его все еще можно использовать для перехода от PPL-CodeGen к PPL-WindowsTCB.
Заключение
В этом блоге показано, как мне удалось внедрить произвольный код в PPL, не требуя прав администратора. Что вы могли бы сделать с этой вновь обретенной силой? На самом деле это не так уж важно для обычного пользователя, но есть некоторые части ОС, такие как Магазин Windows, которые полагаются на PPL для защиты файлов и ресурсов, которые вы не можете изменить как обычный пользователь.Если вы повысите уровень до администратора, а затем внедрите в PPL, вы получите гораздо больше возможностей для атак, таких как CSRSS (с помощью которого вы, безусловно, можете получить выполнение кода ядра) или атаковать Защитник Windows, который работает как PPL Anti-Malware. Я уверен, что со временем большинство вариантов использования PPL будут заменены приложениями в виртуальном безопасном режиме (VSM) и изолированном режиме пользователя (IUM), которые имеют более высокие гарантии безопасности и также считаются границами безопасности, которые Microsoft будет защищать и исправлять.
Сообщал ли я об этих проблемах в Microsoft? Microsoft дала понять, что они не будут исправлять проблемы, затрагивающие только PP и PPL, в бюллетене по безопасности. Без бюллетеня по безопасности исследователь не получает подтверждения о находке, например CVE. Проблема не будет исправлена в текущих версиях Windows, хотя может быть исправлена в следующей основной версии. Ранее подтверждение политики Microsoft по устранению конкретной проблемы безопасности было основано на прецеденте, однако недавно они опубликовали список технологий Windows, которые будут или не будут исправлены в критериях службы безопасности Windows, которые, как показано ниже для Protected Process Light, Microsoft не будет исправлять проблемы, связанные с этой функцией, и платить за них. Поэтому с этого момента я не буду связываться с Microsoft, если обнаружу проблемы, которые, по моему мнению, влияют только на PP или PPL.
Единственная ошибка, о которой я сообщил в Microsoft, была исправлена только потому, что ее можно было использовать для обхода Device Guard. Если подумать, то только исправление для Device Guard несколько странно. Я все еще могу обойти Device Guard, внедрив в PPL и установив кэшированный уровень подписи, и все же Microsoft не исправит проблемы PPL, но исправит проблемы Device Guard. Документ "Критерии службы безопасности Windows" во многом помогает прояснить, что Microsoft будет и что не будет исправлять. Безопасная функция редко бывает изолированной, функция почти наверняка безопасна, потому что другие функции позволяют это сделать.
Во второй части этого блога мы расскажем, как мне удалось взломать процессы Full PP-WindowsTCB, используя еще одну интересную функцию COM.
Источник: https://googleprojectzero.blogspot.com/2018/10/injecting-code-into-windows-protected.html
Автор перевода: yashechka
Переведено специально для https://xss.pro