Это сообщение в статье представляет собой подробный обзор интересного класса логических ошибок в ядре Windows и того, что я сделал, чтобы попытаться исправить это с нашими партнерами в Microsoft. Максимальное влияние класса ошибки - это локальное повышение привилегий, если разработчики ядра и драйверов не принимают во внимание работу диспетчера ввода-вывода при доступе к объектам устройства. В этой статье рассказывается, как я обнаружил класс ошибки и техническую состовляющую. Для получения дополнительной информации о дальнейшем исследовании, исправлении и недопущении написания нового кода с классом ошибки смотрите сообщение в блоге MSRC (https://blogs.technet.microsoft.com...-i-o-manager-a-variant-finding-collaboration/).
Технические подробности
Впервые я наткнулся на класс ошибки, пытаясь эксплуатировать проблему 779 (https://bugs.chromium.org/p/project-zero/issues/detail?id=779). Эта проблема была связана с файлом TOCTOU, который обошел политику ограничения загрузки пользовательских шрифтов. Защитная политика была введена в Windows 10, чтобы ограничить воздействие уязвимостей, используемых для повреждения памяти шрифтов. Обычно было бы тривиально использовать проблему с TOCTOU файла, используя комбинацию символических ссылок файлов и Object Manager. Эксплойт, использующий символические ссылки, работал под обычным пользователем, но не внутри песочницы. Вместо того, чтобы тратить слишком много времени на эту незначительную проблему, я использовал ее без символических ссылок, используя каталоги теневых объектов, а Microsoft исправила ее как CVE-2016-3219. Я добавил заметку о неожиданном поведении в свой список тем, над которыми нужно работать позже.
Я решил вернуться назад и более подробно изучить неожиданное поведение. Код, который давал сбой, был похож на следующий:
Когда этот код пытается открыть файл с символической ссылкой диспетчера объектов в пути, вызов IoCreateFile завершился ошибкой с STATUS_OBJECT_NAME_NOT_FOUND. Дальнейшие поиски привели меня к обнаружению источника ошибки в ObpParseSymbolicLink, которая выглядит следующим образом:
Неудачная проверка является частью мер по защите символических ссылок, которые Microsoft представила в Windows 10. Я создавал символическую ссылку внутри песочницы, которая устанавливала SANDBOX_FLAG в структуре объекта. Эта проверка выполняется при разборе символической ссылки при открытии файла шрифта. С установленным флагом песочницы ядро также вызывает RtlIsSandboxedToken, чтобы определить, находится ли вызывающий код по-прежнему внутри песочницы. Поскольку вызов для открытия файла шрифта находится в изолированном потоке процесса, RtlIsSandboxedToken должен вернуть TRUE, и функция продолжит работу. Вместо этого он возвращал FALSE, что заставляло ядро думать, что вызов исходил от более привилегированного процесса, и возвращал STATUS_OBJECT_NAME_NOT_FOUND для защиты от любой эксплуатации.
В этот момент я понял, что мой эксплойт не заработал, но не понял почему. В частности, я не понимал, почему RtlIsSandboxToken возвращает FALSE. Изучение функции дало мне важное понимание:
Важным параметром является AccessMode, который имеет тип KPROCESSOR_MODE и может иметь одно из двух значений UserMode или KernelMode. Если для параметра AccessMode было задано значение KernelMode, функция автоматически вернет FALSE, указывая, что текущий вызывающий объект не находится в изолированной программной среде. Точка останова для этой функции в отладчике ядра подтвердила, что AccessMode был установлен на KernelMode при вызове из моего эксплойта. Если бы этот параметр всегда был установлен на KernelMode, как RtlIsSandboxToken когда-либо возвращал TRUE? Чтобы понять, как работает ядро, давайте более подробно рассмотрим, что представляет собой параметр AccessMode.
Предыдущий режим доступа
С каждым потоком в Windows связан предыдущий режим доступа. Этот предыдущий режим доступа сохраняется в элементе PreviousMode структуры KTHREAD. Доступ к члену осуществляется третьими сторонами с помощью ExGetPreviousMode, и он возвращает тип KPROCESSOR_MODE. Предыдущий режим доступа установлен на UserMode, если поток пользовательского режима выполняет код ядра из-за перехода системного вызова. В качестве примера на следующей диаграмме показан вызов из приложения пользовательского режима к системному вызову NtOpenFile через функцию-заглушку диспетчеризации системного вызова в NTDLL.
Обратите внимание, что предыдущий режим всегда установлен на UserMode, даже когда код выполняется внутри системного вызова NtOpenFile в пространстве памяти ядра. Напротив, KernelMode устанавливается, если поток является системным потоком (в системном процессе) или из-за перехода системного вызова из режима ядра. На следующей диаграмме показан переход, когда драйвер устройства (который уже работает в режиме ядра) вызывает системный вызов ZwOpenFile, что приводит к выполнению NtOpenFile.
На схеме приложение пользовательского режима вызывает функцию внутри драйвера устройства, например, используя системный вызов NtFsControlFile. Предыдущий режим равен UserMode во время вызова драйвера устройства. Однако, если драйвер устройства вызывает ZwOpenFile, ядро имитирует переход системного вызова, это приводит к изменению предыдущего режима на KernelMode и выполнению кода системного вызова NtOpenFile.
С точки зрения безопасности предыдущий режим доступа влияет на две важные, но принципиально разные проверки безопасности в ядре, проверку доступа к безопасности (SecAC) и проверку доступа к памяти (MemAC). SecAC используется для вызовов API, предоставляемых контрольным монитором безопасности, например SeAccessCheck или SePrivilegeCheck. Эти API безопасности используются, чтобы определить, есть ли у вызывающего пользователя права на доступ к ресурсу. Обычно API-интерфейсы принимают параметр AccessMode, если этот параметр имеет значение KernelMode, тогда проверки доступа проходят автоматически, что может быть уязвимостью безопасности, если пользователь обычно не может получить доступ к ресурсу. Мы уже видели этот вариант использования в RtlIsSandboxToken, API явно проверил, что AccessMode является KernelMode, и вернул FALSE. Даже без ярлыка при передаче KernelMode в SeAccessCheck вызов будет успешным независимо от токена доступа вызывающего кода, и RtlIsSandboxToken вернет FALSE.
MemAC используется для того, чтобы пользовательское приложение не могло передавать указатели на расположение адреса ядра. Если AccessMode - UserMode, тогда все адреса памяти, переданные системному вызову/операции, должны быть проверены на то, что они меньше MmUserProbeAddress или с помощью таких функций, как ProbeForRead ProbeForWrite. Опять же, если эта проверка неверна, может произойти повышение привилегий, поскольку пользователь может обманом заставить код ядра читать или записывать в привилегированные области памяти ядра. Обратите внимание, что не все API-интерфейсы ядра выполняют MemAC, например, SeAccessCheck предполагает, что вызывающий объект уже проверил параметры, параметр AccessMode используется только для определения необходимости обхода проверки безопасности.
Сохранение предыдущего режима доступа в потоке создает проблему, поскольку нет способа различить SecAC и MemAC для API ядра. API может намеренно отключить SecAC и случайно отключить MemAC, что приведет к проблемам с безопасностью, и наоборот. Давайте подробнее рассмотрим, как диспетчер ввода-вывода пытается решить эту проблему проверки несоответствия доступа.
Проверка доступа IO Manager
IO Manager предоставляет две основные группы API для прямого доступа к файлам. Первые API - это системные вызовы NtCreateFile/ZwCreateFile или NtOpenFile/ZwOpenFile. Системные вызовы в основном предназначены для использования приложениями пользовательского режима, но при необходимости могут быть вызваны из режима ядра. Другие API доступны только для вызывающих объектов режима ядра, IoCreateFile и IoCreateFileEx.
Если вы сравните реализации двух основных API, то обнаружите, что они представляют собой простые обертки для пересылки внутренней функции IopCreateFile. По умолчанию IopCreateFile использует предыдущий режим текущего потока, чтобы определить, следует ли выполнять MemAC и SecAC. Например, при вызове IopCreateFile через NtCreateFile из процесса пользовательского режима ядро выполняет MemAC и SecAC, поскольку предыдущим режимом будет UserMode. Если режим ядра вызывает ZwCreateFile, тогда для предыдущего режима устанавливается значение KernelMode, и SecAC и MemAC отключены.
IoCreateFile может быть вызван только из кода режима ядра, и здесь не задействован переход системных вызовов, поэтому любые вызовы будут использовать любой предыдущий режим, установленный в потоке. Если IoCreateFile вызывается из потока с предыдущим режимом, установленным на UserMode, это означает, что будут выполняться SecAC и MemAC. Применение MemAC особенно проблематично, поскольку это означает, что код ядра не может передавать указатели режима ядра в IoCreateFile, что очень затрудняет использование API. Однако вызывающий IoCreateFile не может просто изменить предыдущий режим потока на KernelMode, поскольку тогда SecAC будет отключен.
IoCreateFile решает эту проблему, указывая специальные флаги, которые можно передать с помощью параметра Options. Этот параметр пересылается в IopCreateFile, но не предоставляется через системный вызов NtCreateFile. Возвращаясь к нашей проблеме со шрифтом, WIN32K вызывает IoCreateFile и передает флаги параметров IO_NO_PARAMETER_CHECKING (INPC) и IO_FORCE_ACCESS_CHECK (IFAC).
INPC задокументирован как:
В разделе примечаний INPC расширяется дальше:
Это проясняет его цель, он отключает MemAC, позволяя коду ядра передавать указатели в память ядра в качестве параметров функции. В качестве побочного продукта он также отключает большую часть проверки параметров, таких как проверяемые несовместимые комбинации флагов. Существует отдельный, не задокументированный должным образом, флаг IO_CHECK_CREATE_PARAMETERS, который включает только проверку флага параметров, но не MemAC.
IFAC , с другой стороны, задокументирован как:
Это означает, что флаг повторно включает SecAC. Имеет смысл, если бы вызывающий код был системным потоком с предыдущим режимом, установленным на KernelMode, но зачем нам повторно включать SecAC, если мы вызываем из UserMode? В этом и кроется начало понимания исходного неожиданного поведения, как мы можем видеть в некотором упрощенном коде из IopCreateFile.
Этот код показывает, что если указан INPC, то AccessMode для всех последующих вызовов устанавливается на KernelMode. Поэтому указание этой опции отключает не только MemAC, но и SecAC. Стоит отметить, что предыдущий режим потока не изменяется, только значение AccessMode, которое передается вперед в ObOpenObjectByName. IopCreateFile делегирует проверку указателя диспетчеру объектов, поэтому единственный способ добиться этого - отключить все проверки. Крайне важно, что IFAC не проверяется, он только передается внутри структуры контекста синтаксического анализа, с чем должен иметь дело остальной менеджер ввода-вывода.
Это еще не конец истории, также можно вызвать ZwCreateFile и передать специальный флаг OBJ_FORCE_ACCESS_CHECK (OFAC) внутри структуры OBJECT_ATTRIBUTES и обеспечить выполнение проверки доступа, даже если предыдущий режим доступа установлен на KernelMode. Поскольку вы не можете передать IFAC через ZwCreateFile, и в IopCreateFile не проверяется флаг OFAC, он должен быть в ObOpenObjectByName. На самом деле он немного глубже: сначала все параметры обрабатываются на основе AccessMode, переданного в ObOpenObjectByName, затем вызывается ObpLookupObjectName, который проверяет флаг OFAC, если он установлен, AccessMode принудительно возвращается в UserMode.
Теперь мы наконец можем понять, почему мы получили неожиданное поведение при открытии файла шрифта. Анализ символьной ссылки происходит внутри диспетчера объектов, а не диспетчера ввода-вывода, поэтому он не знает флаг IFAC. IopCreateFile сказал диспетчеру объектов выполнить все проверки, как если бы предыдущий режим доступа был KernelMode, поэтому это значение передается в ObpParseSymbolicLink, которое передается в RtlIsSandboxToken, что справедливо указывает на то, что он не запущен в изолированном процессе. Однако, как только файл действительно открывается, срабатывает флаг IFAC и гарантирует, что SecAC по-прежнему применяется к файлу. Если бы вызывающий код также указал OFAC, тогда символическая ссылка работала бы, поскольку синтаксический анализ происходил бы во время операции поиска, которая была принудительно установлена в UserMode.
Это само по себе является интересным результатом, по сути, любая операция, которая вызывается во время операции анализа диспетчера объектов, которая доверяет значению AccessMode, отключит проверки безопасности, если не указано OFAC. Однако это не та ошибка, о которой идет речь в этом блоге, для этого нам нужно углубиться, чтобы понять, как IFAC работает внутри IO Manager.
Разбор устройства ввода-вывода
Ответственность диспетчера объектов за открытие файла заканчивается, когда он находит именованный объект устройства в пространстве имен диспетчера объектов. Диспетчер объектов ищет функцию синтаксического анализа для типа устройства, которым является IopParseDevice, а затем передает всю информацию, о которой он знает. Это включает значение AccessMode, которое, как мы знаем, установлено на KernelMode, оставшийся путь для синтаксического анализа и буфер контекста синтаксического анализа, который включает параметр Options. Функция IopParseDevice выполняет некоторые собственные проверки безопасности, такие как проверка обхода устройства, выделяет новый пакет запроса ввода-вывода (IRP) и вызывает драйвер, ответственный за объект устройства.
Структура IRP содержит поле RequestorMode, которое отражает AccessMode файловой операции. Причина наличия поля RequestorMode заключается в том, что IRP может отправляться асинхронно. Поток, который обрабатывает операцию ввода-вывода, может не быть потоком, запустившим операцию ввода-вывода. Теперь вы можете догадаться, что именно здесь в игру вступает IFAC, возможно, менеджер ввода-вывода устанавливает RequestorMode в UserMode? Если вы действительно проверите это в драйвере ядра при доступе из IoCreateFile с помощью INPC, вы обнаружите, что в этом поле все еще установлено значение KernelMode, так что это не ответ.
Тип выполняемой операции и конкретные параметры операции передаются в структуре местоположения стека ввода-вывода, которая расположена сразу после структуры IRP. В случае открытия файла основным типом операции является IRP_MJ_CREATE, и используется поле Create структуры IO_STACK_LOCATION. Здесь на помощь приходит IFAC: если флаг указан для IopCreateFile, тогда в параметре Flags местоположения стека ввода-вывода будет установлен новый флаг SL_FORCE_ACCESS_CHECK (SFAC). Драйвер файловой системы должен убедиться, что он проверяет этот флаг, и не полагаться на то, что RequestorMode установлен в UserMode. Драйвер NTFS знает об этом и имеет следующий код:
NtfsEffectiveMode вызывается любой операцией, которая будет выполнять функцию, связанную с безопасностью. Это гарантирует, что SecAC по-прежнему выполняется, даже если вызывающий объект находился в режиме ядра, пока был передан флаг IFAC. Драйвер файловой системы NTFS является фундаментальной частью ОС Windows и тесно взаимодействует с диспетчером ввода-вывода, поэтому неудивительно, что он знает, как поступать правильно. Однако в Windows все драйверы являются драйверами файловой системы, даже если они явно не реализуют файловую систему.
Я подумал, что было бы интересно узнать, сколько драйверов Microsoft и сторонних производителей действительно выполнили правильные проверки, или все они просто доверяли RequestorMode и основывали свои решения по безопасности на нем?
Определение класса ошибки
Наконец, мы должны определить класс ошибки. Для существования уязвимости, связанной с повышением привилегий, необходимо наличие двух отдельных компонентов.
- Инициатор режима ядра (код, вызывающий IoCreateFile или IoCreateFileEx), который устанавливает флаги INPC и IFAC, но не устанавливает OFAC. Это может быть драйвер или само ядро.
- Уязвимый Receiver, который использует RequestorMode во время обработки IRP_MJ_CREATE для принятия решения о безопасности, но не проверяет также флаги для SFAC.
В следующей таблице приводится сводка API, когда предыдущий режим вызывающего потока установлен на UserMode. Таблица включает в себя состояние опций ввода, INPC, IFAC и OFAC, а также соответствующий RequestorMode IRP и флаг SFAC. Я выделил, когда вызовы являются полезным инициатором.
Стоит отметить, что любые вызовы, в которые не передается IFAC, могут быть уязвимы для уязвимости привилегированного доступа к файлам, поскольку без сгенерированного флага SFAC даже NTFS не будет выполнять проверки безопасности. Есть и другие функции, похожие на IoCreateFile, такие как FltCreateFileEx, которые используются в особых случаях, но все они имеют похожие свойства. Также обратите внимание, что в таблице правила для IoCreateFileEx немного отличаются. Хотя это не задокументировано, IoCreateFileEx всегда передает параметр INPC в IopCreateFile, поэтому, если не указан флаг OFAC, он всегда будет выполнять свои операции с предыдущим режимом доступа, установленным на KernelMode.
Идеальный инициатор - это тот, который открывает произвольный путь от пользователя и предоставляет полный контроль над всеми параметрами IoCreateFile, а дескриптор открытого файла возвращается обратно в пользовательский режим. Однако в зависимости от приемника полный контроль может не требоваться.
Получатель может выполнить ряд действий при получении IRP. Общим для драйверов файловой системы является анализ оставшегося имени файла и выполнение на его основе некоторых дальнейших действий, таких как открытие другого файла. Другая возможность - разобрать блок расширенных атрибутов (EA) и выполнить какое-то действие на его основе. Возможно, открытие объекта устройства обычно требует проверки доступа, которую обходит установка RequestorMode на KernelMode.
Примеры
Вот несколько примеров, которые я нашел как инициаторов, так и получателей. Все это основано на коде, поставляемом с Windows 10 1709, что на две версии ниже того, что доступно сегодня (1809), но многие из этих примеров все еще существуют в последних версиях Windows, а также в Windows 7 и 8. Все примеры представляют собой код Microsoft, поэтому сторонний разработчик, вероятно, еще меньше понимает поведение системы.
Чтобы найти эти примеры, я не использовал никаких специальных инструментов статического анализа, вместо этого я просто искал их вручную. Я оставил более глубокое расследование Microsoft.
Получатели
Поиск получателей может быть намного сложнее, чем инициаторов, поскольку нет импортированной функции для поиска, которая дает четкий сигнал для поиска. Вместо этого я искал драйверы, которые импортировали IoCreateDevice, чтобы убедиться, что драйвер предоставляет какое-либо устройство. Затем я отфильтровал импортированные драйверы API-интерфейсы на те, которые принимали явный параметр AccessMode, например SeAccessCheck или ObReferenceObjectByHandle. Конечно, это не сильно ограничивало количество драйверов, поэтому в конечном итоге мне пришлось вручную проанализировать драйверы, которые выглядели наиболее интересными. В моем анализе "настоящие" драйверы файловой системы, такие как NTFS и FAT, всегда кажутся правильными.
Чтобы эксплуатировать его, совместимый инициатор должен иметь возможность предоставить EA для файла процесса. Затем необходимо создать файл сокета, который ссылается на этот файл процесса, и выполнить операцию чтения/записи, чтобы заставить APC выполнить. В современных версиях Windows 10 вам также придется беспокоиться о SMEP и Kernel CFG. Если вы укажете подпрограмме APC на адрес пользовательского режима, ядро будет проверять ошибки, когда оно переходит на выполнение APC в режиме KernelMode, как показано на следующем снимке экрана, который я создал с помощью специального инициатора для настройки WS2IFSL.
Хотя NPFS правильно проверяет SFAC, он использует RequestorMode, чтобы определить, разрешено ли вызывающему коду указать произвольный блок EA. Обычно при открытии именованного канала драйвер записывает вызывающий PID и идентификатор сеанса. Затем эта информация может быть предоставлена через API, такие как GetNamedPipeClientProcessId. Если вызывающий код является UserMode, то коду не разрешено устанавливать блок EA, но если это KernelMode, можно использовать произвольный блок EA, что означает, что поля PID и идентификатора сеанса могут быть подделаны.
Такое поведение используется, чтобы позволить драйверу SMB установить поле имени компьютера и идентификатор сеанса. Если какой-то сервис доверяет этой информации, ее можно использовать для повышения привилегий. Чтобы эксплуатировать это, вы должны иметь возможность установить произвольный EA как KernelMode, а чтобы сделать что-нибудь интересное, вам потребуется доступ к открытому дескриптору.
Инициаторы
Чтобы найти инициаторов, я просмотрел ядро и драйверы для любых функций, которые вызывали IoCreateFile и другие, и провел базовую проверку параметров вызова для флагов параметров и атрибутов объектов. С подходящими кандидатами я смог более внимательно изучить, на какие параметры может влиять пользователь. Поиск инициаторов относительно тривиален, если вы понимаете класс ошибки, поскольку вы можете быстро сузить цели, просто просматривая импортированные вызовы целевых методов.
Класс NTOSKRNL NtSetInformationFile FileRenameInformation
При переименовании файла вы можете указать произвольный путь, даже если файл не может находиться на томе, отличном от исходного. Функция IopOpenLinkOrRenameTarget вызывается, чтобы сначала открыть целевой путь, используя IoCreateFileEx, передающий INPC и обычно IFAC (он также устанавливает IO_OPEN_TARGET_DIRECTORY, но это не важно для операции). Этот инициатор позволяет указать только полный путь.
Драйвер сервера SMBv2
Серверы SMB будут открывать файлы на общем ресурсе с помощью IoCreateFileEx, например, в Smb2CreateFile. Он указывает IFAC, но не INPC, потому что вызов выполняется в системном потоке, поэтому предыдущий режим доступа уже является KernelMode. Обычно невозможно перенаправить создание файла на произвольный путь диспетчера объектов NT, поскольку сервер передает относительный путь к дескриптору открытого тома. Хотя вы можете добавить точку монтирования в каталог в файловой системе и получить доступ к серверу локально, ядро намеренно ограничивает целевое устройство ограниченным набором типов.
В реализации была ошибка, которая позволяет обойти проверку устройства, которая позволяет использовать локальную точку монтирования для перенаправления сервера SMB для открытия любого файла устройства. Драйвер SMBv2 предъявляет особые требования к символическим ссылкам NTFS: он должен возвращать информацию о ссылках клиенту для обработки. Для поддержки символьной ссылки сервер передает флаг параметров IO_STOP_ON_SYMLINK, как показано ниже.
Если IoCreateFileEx возвращает STATUS_STOPPED_ON_SYMLINK, сервер извлекает возвращенную структуру REPARSE_DATA_BUFFER из IO_STATUS_BLOCK и передает ее служебной функции SrvGraftName. Буфер повторной обработки может быть либо точкой монтирования, либо символической ссылкой NTFS. Если это символическая ссылка, то SrvGraftName снова возвращает STATUS_STOPPED_ON_SYMLINK, что позволяет серверу вернуть буфер вызывающему коду. Если буфер повторного анализа является точкой монтирования, то SrvGraftName строит новый абсолютный путь только на основе строки, найденной в REPARSE_DATA_BUFFER, он не проверяет целевое устройство. Сервер повторно отправляет открытый запрос с новым абсолютным путем, который теперь может указывать на любое устройство в системе.
Этот инициатор был лучшим, что я нашел во время своего анализа. Вы можете указать почти все аргументы IoCreateFileEx, включая буфер EA. Это позволяет вам инициализировать драйвер, для которого требуется EA (например, WS2IFSL), который открывает гораздо больше возможностей для атак. Поскольку он выполняется в системном потоке, RequestorMode и предыдущий режим потока устанавливаются на KernelMode, что может привести к другой интересной поверхности для атаки.
Впечатляет, что это не идеальный инициатор, но открытое устройство должно поддерживать определенные допустимые IRP, такие как IRP_MJ_GET_INFORMATION_FILE, в противном случае сервер не вернет действительный дескриптор вызывающему коду. Без этой проверки было бы тривиально использовать WS2IFSL, поскольку вы могли бы выполнить операцию чтения/записи, чтобы заставить APC выполняться в режиме ядра. Даже если вы можете вернуть дескриптор файла, сервер SMB намеренно ограничивает коды управления вводом-выводом, которые вы можете отправлять, что ограничивает многие интересные вещи, которые вы могли бы сделать с этой уязвимостью. Несмотря на это, я посчитал это серьезной проблемой, поэтому сообщил об этом непосредственно в MSRC. Баг был исправлен как CVE-2018-0749 с использованием специального параметра Extra Creation Parameter, который отфильтровывает все другие точки повторной обработки, кроме символических ссылок.
Следующие шаги
Хотя я считал, что это серьезный класс ошибок, оказалось, что найти подходящую пару инициатор/получатель было очень сложно. В своем исследовании я не нашел ни одной пары, которая дала бы прямую эскалацию привилегий. Лучшая пара, которую я обнаружил, - это сочетание инициатора SMBv2 с подделкой идентификатора процесса NPFS. Хотя мне не удалось идентифицировать службу, которая будет использовать PID клиента для какой-либо операции, связанной с безопасностью, вполне возможно, что существует сторонняя служба.
Я мог бы снова добавить эту проблему в свой список интересных или неожиданных действий, чтобы посмотреть, смогу ли я использовать ее позже. Но вместо этого я решил поговорить со своими контактами в MSRC, чтобы узнать, можем ли мы сотрудничать. Вместе с MSRC я написал документ, объясняющий класс ошибки и описывающий некоторые из моих выводов. В то же время я сообщил о проблеме с сервером SMB по обычным каналам, поскольку это была самая серьезная проблема, которую я обнаружил. Это привело к встречам с различными командами на Bluehat 2017 в Редмонде, где был сформирован план для Microsoft использовать доступ к исходному коду, чтобы обнаружить степень этого класса ошибок в ядре Windows и базе кода драйверов. Обратите внимание, у меня не было доступа к исходному коду, эта часть расследования была делегирована MSRC, результаты которого опубликованы в их блоге.
Стоит отметить, что хотя я применил стандартный 90-дневный крайний срок раскрытия к отчету, я не применил явный крайний срок к отчету о классе ошибок. Без единой ошибки, на которую можно было бы указать, было бы сложно и, вероятно, непродуктивно обеспечить соблюдение такого срока. Однако я убедился, что MSRC согласилась опубликовать технические подробности по этому вопросу независимо от результата. Вот почему мы пишем об этом сейчас, через 12 месяцев после предоставления отчета.
Заключение
Всегда интересно найти новый класс ошибок в Windows, за которым можно будет охотиться.
По счастливой случайности все обернулось не так серьезно, как могло бы быть. Хотя было бы разумно указать два отдельных режима доступа, один для MemAC и один для SecAC, это не то, что использовалось в исходной конструкции NT. Для обратной совместимости маловероятно, что поведение будет изменено. Этот класс ошибок связан как с плохой документацией, так и с техническими проблемами, поскольку, хотя поведение нельзя изменить, оно также плохо документировано.
Если вы посмотрите документацию по IoCreateFile, вы увидите новое примечание:
Это замечание было добавлено совсем недавно. В большинстве документации для разработчиков Microsoft на GitHub вы даже можете найти этот коммит.
Вполне вероятно, что любые ошибки, обнаруженные MSRC, будут исправлены только в последних версиях Windows 10, поэтому, если вы разработчик, вам следует прочитать сообщение в блоге Microsoft, чтобы понять, как избежать этих проблем в ваших драйверах, а также способы обнаружения этимх различные проблемы в вашей кодовой базе. Исследователям в области безопасности следует помнить о другом, когда вы рассматриваете новый драйвер ядра Windows.
Я хотел бы поблагодарить Стивена Хантера и Гэвина Томаса из MSRC, которые были моими основными контактными лицами для исправления этого класса ошибок.
Источник: https://googleprojectzero.blogspot.com/2019/03/windows-kernel-logic-bug-class-access.html
Автор перевода: yashechka
Переведено специально для https://xss.pro
Технические подробности
Впервые я наткнулся на класс ошибки, пытаясь эксплуатировать проблему 779 (https://bugs.chromium.org/p/project-zero/issues/detail?id=779). Эта проблема была связана с файлом TOCTOU, который обошел политику ограничения загрузки пользовательских шрифтов. Защитная политика была введена в Windows 10, чтобы ограничить воздействие уязвимостей, используемых для повреждения памяти шрифтов. Обычно было бы тривиально использовать проблему с TOCTOU файла, используя комбинацию символических ссылок файлов и Object Manager. Эксплойт, использующий символические ссылки, работал под обычным пользователем, но не внутри песочницы. Вместо того, чтобы тратить слишком много времени на эту незначительную проблему, я использовал ее без символических ссылок, используя каталоги теневых объектов, а Microsoft исправила ее как CVE-2016-3219. Я добавил заметку о неожиданном поведении в свой список тем, над которыми нужно работать позже.
Я решил вернуться назад и более подробно изучить неожиданное поведение. Код, который давал сбой, был похож на следующий:
C:
HANDLE OpenFilePath(LPCWSTR pwzPath) {
UNICODE_STRING Path;
OBJECT_ATTRIBUTES ObjectAttributes;
HANDLE FileHandle;
NTSTATUS status;
RtlInitUnicodeString(&Path, pwzPath);
InitializeObjectAttributes(&ObjectAttributes,
&Path,
OBJ_KERNEL_HANDLE | OBJ_CASE_INSENSITIVE);
status = IoCreateFile(
&FileHandle,
GENERIC_READ,
&ObjectAttributes,
// ...
FILE_OPEN,
FILE_NON_DIRECTORY_FILE,
// ...
IO_NO_PARAMETER_CHECKING | IO_FORCE_ACCESS_CHECK);
if (NT_ERROR(status))
return NULL;
return FileHandle;
}
Когда этот код пытается открыть файл с символической ссылкой диспетчера объектов в пути, вызов IoCreateFile завершился ошибкой с STATUS_OBJECT_NAME_NOT_FOUND. Дальнейшие поиски привели меня к обнаружению источника ошибки в ObpParseSymbolicLink, которая выглядит следующим образом:
C:
NTSTATUS ObpParseSymbolicLink(POBJECT_SYMBOLIC_LINK Object,
PACCESS_STATE AccessState,
KPROCESSOR_MODE AccessMode) {
if (Object->Flags & SANDBOX_FLAG
&& !RtlIsSandboxedToken(AccessState->SubjectSecurityContext, AccessMode))
return STATUS_OBJECT_NAME_NOT_FOUND;
// ...
}
Неудачная проверка является частью мер по защите символических ссылок, которые Microsoft представила в Windows 10. Я создавал символическую ссылку внутри песочницы, которая устанавливала SANDBOX_FLAG в структуре объекта. Эта проверка выполняется при разборе символической ссылки при открытии файла шрифта. С установленным флагом песочницы ядро также вызывает RtlIsSandboxedToken, чтобы определить, находится ли вызывающий код по-прежнему внутри песочницы. Поскольку вызов для открытия файла шрифта находится в изолированном потоке процесса, RtlIsSandboxedToken должен вернуть TRUE, и функция продолжит работу. Вместо этого он возвращал FALSE, что заставляло ядро думать, что вызов исходил от более привилегированного процесса, и возвращал STATUS_OBJECT_NAME_NOT_FOUND для защиты от любой эксплуатации.
В этот момент я понял, что мой эксплойт не заработал, но не понял почему. В частности, я не понимал, почему RtlIsSandboxToken возвращает FALSE. Изучение функции дало мне важное понимание:
C:
BOOLEAN RtlIsSandboxedToken(PSECURITY_SUBJECT_CONTEXT SubjectSecurityContext,
KPROCESSOR_MODE AccessMode) {
NTSTATUS AccessStatus;
ACCESS_MASK GrantedAccess;
if (AccessMode == KernelMode)
return FALSE;
if (SeAccessCheck(
SeMediumDaclSd,
SubjectSecurityContext,
FALSE,
READ_CONTROL,
0,
NULL,
&RtlpRestrictedMapping,
AccessMode,
&GrantedAccess,
&AccessStatus)) {
return FALSE;
}
return TRUE;
}
Важным параметром является AccessMode, который имеет тип KPROCESSOR_MODE и может иметь одно из двух значений UserMode или KernelMode. Если для параметра AccessMode было задано значение KernelMode, функция автоматически вернет FALSE, указывая, что текущий вызывающий объект не находится в изолированной программной среде. Точка останова для этой функции в отладчике ядра подтвердила, что AccessMode был установлен на KernelMode при вызове из моего эксплойта. Если бы этот параметр всегда был установлен на KernelMode, как RtlIsSandboxToken когда-либо возвращал TRUE? Чтобы понять, как работает ядро, давайте более подробно рассмотрим, что представляет собой параметр AccessMode.
Предыдущий режим доступа
С каждым потоком в Windows связан предыдущий режим доступа. Этот предыдущий режим доступа сохраняется в элементе PreviousMode структуры KTHREAD. Доступ к члену осуществляется третьими сторонами с помощью ExGetPreviousMode, и он возвращает тип KPROCESSOR_MODE. Предыдущий режим доступа установлен на UserMode, если поток пользовательского режима выполняет код ядра из-за перехода системного вызова. В качестве примера на следующей диаграмме показан вызов из приложения пользовательского режима к системному вызову NtOpenFile через функцию-заглушку диспетчеризации системного вызова в NTDLL.
Обратите внимание, что предыдущий режим всегда установлен на UserMode, даже когда код выполняется внутри системного вызова NtOpenFile в пространстве памяти ядра. Напротив, KernelMode устанавливается, если поток является системным потоком (в системном процессе) или из-за перехода системного вызова из режима ядра. На следующей диаграмме показан переход, когда драйвер устройства (который уже работает в режиме ядра) вызывает системный вызов ZwOpenFile, что приводит к выполнению NtOpenFile.
На схеме приложение пользовательского режима вызывает функцию внутри драйвера устройства, например, используя системный вызов NtFsControlFile. Предыдущий режим равен UserMode во время вызова драйвера устройства. Однако, если драйвер устройства вызывает ZwOpenFile, ядро имитирует переход системного вызова, это приводит к изменению предыдущего режима на KernelMode и выполнению кода системного вызова NtOpenFile.
С точки зрения безопасности предыдущий режим доступа влияет на две важные, но принципиально разные проверки безопасности в ядре, проверку доступа к безопасности (SecAC) и проверку доступа к памяти (MemAC). SecAC используется для вызовов API, предоставляемых контрольным монитором безопасности, например SeAccessCheck или SePrivilegeCheck. Эти API безопасности используются, чтобы определить, есть ли у вызывающего пользователя права на доступ к ресурсу. Обычно API-интерфейсы принимают параметр AccessMode, если этот параметр имеет значение KernelMode, тогда проверки доступа проходят автоматически, что может быть уязвимостью безопасности, если пользователь обычно не может получить доступ к ресурсу. Мы уже видели этот вариант использования в RtlIsSandboxToken, API явно проверил, что AccessMode является KernelMode, и вернул FALSE. Даже без ярлыка при передаче KernelMode в SeAccessCheck вызов будет успешным независимо от токена доступа вызывающего кода, и RtlIsSandboxToken вернет FALSE.
MemAC используется для того, чтобы пользовательское приложение не могло передавать указатели на расположение адреса ядра. Если AccessMode - UserMode, тогда все адреса памяти, переданные системному вызову/операции, должны быть проверены на то, что они меньше MmUserProbeAddress или с помощью таких функций, как ProbeForRead ProbeForWrite. Опять же, если эта проверка неверна, может произойти повышение привилегий, поскольку пользователь может обманом заставить код ядра читать или записывать в привилегированные области памяти ядра. Обратите внимание, что не все API-интерфейсы ядра выполняют MemAC, например, SeAccessCheck предполагает, что вызывающий объект уже проверил параметры, параметр AccessMode используется только для определения необходимости обхода проверки безопасности.
Сохранение предыдущего режима доступа в потоке создает проблему, поскольку нет способа различить SecAC и MemAC для API ядра. API может намеренно отключить SecAC и случайно отключить MemAC, что приведет к проблемам с безопасностью, и наоборот. Давайте подробнее рассмотрим, как диспетчер ввода-вывода пытается решить эту проблему проверки несоответствия доступа.
Проверка доступа IO Manager
IO Manager предоставляет две основные группы API для прямого доступа к файлам. Первые API - это системные вызовы NtCreateFile/ZwCreateFile или NtOpenFile/ZwOpenFile. Системные вызовы в основном предназначены для использования приложениями пользовательского режима, но при необходимости могут быть вызваны из режима ядра. Другие API доступны только для вызывающих объектов режима ядра, IoCreateFile и IoCreateFileEx.
Если вы сравните реализации двух основных API, то обнаружите, что они представляют собой простые обертки для пересылки внутренней функции IopCreateFile. По умолчанию IopCreateFile использует предыдущий режим текущего потока, чтобы определить, следует ли выполнять MemAC и SecAC. Например, при вызове IopCreateFile через NtCreateFile из процесса пользовательского режима ядро выполняет MemAC и SecAC, поскольку предыдущим режимом будет UserMode. Если режим ядра вызывает ZwCreateFile, тогда для предыдущего режима устанавливается значение KernelMode, и SecAC и MemAC отключены.
IoCreateFile может быть вызван только из кода режима ядра, и здесь не задействован переход системных вызовов, поэтому любые вызовы будут использовать любой предыдущий режим, установленный в потоке. Если IoCreateFile вызывается из потока с предыдущим режимом, установленным на UserMode, это означает, что будут выполняться SecAC и MemAC. Применение MemAC особенно проблематично, поскольку это означает, что код ядра не может передавать указатели режима ядра в IoCreateFile, что очень затрудняет использование API. Однако вызывающий IoCreateFile не может просто изменить предыдущий режим потока на KernelMode, поскольку тогда SecAC будет отключен.
IoCreateFile решает эту проблему, указывая специальные флаги, которые можно передать с помощью параметра Options. Этот параметр пересылается в IopCreateFile, но не предоставляется через системный вызов NtCreateFile. Возвращаясь к нашей проблеме со шрифтом, WIN32K вызывает IoCreateFile и передает флаги параметров IO_NO_PARAMETER_CHECKING (INPC) и IO_FORCE_ACCESS_CHECK (IFAC).
INPC задокументирован как:
"[Если указаны] параметры для этого вызова не должны проверяться до попытки отправить запрос на создание. Создателям драйверов следует использовать этот флаг с осторожностью, поскольку некоторые недопустимые параметры могут вызвать сбой системы. Для получения дополнительной информации см. Примечания".
В разделе примечаний INPC расширяется дальше:
"Флаг параметров IO_NO_PARAMETER_CHECKING может быть полезен, если драйвер выдает запрос на создание в режиме ядра от имени операции, инициированной приложением пользовательского режима. Поскольку запрос происходит в контексте пользовательского режима, диспетчер ввода-вывода по умолчанию проверяет предоставленные значения параметров, что может вызвать нарушение прав доступа, если параметры являются адресами режима ядра. Этот флаг позволяет вызывающему коду переопределить это поведение по умолчанию и избежать нарушения прав доступа".
Это проясняет его цель, он отключает MemAC, позволяя коду ядра передавать указатели в память ядра в качестве параметров функции. В качестве побочного продукта он также отключает большую часть проверки параметров, таких как проверяемые несовместимые комбинации флагов. Существует отдельный, не задокументированный должным образом, флаг IO_CHECK_CREATE_PARAMETERS, который включает только проверку флага параметров, но не MemAC.
IFAC , с другой стороны, задокументирован как:
"Диспетчер ввода-вывода должен проверить запрос на создание по дескриптору безопасности файла".
Это означает, что флаг повторно включает SecAC. Имеет смысл, если бы вызывающий код был системным потоком с предыдущим режимом, установленным на KernelMode, но зачем нам повторно включать SecAC, если мы вызываем из UserMode? В этом и кроется начало понимания исходного неожиданного поведения, как мы можем видеть в некотором упрощенном коде из IopCreateFile.
C:
NTSTATUS IopCreateFile(PHANDLE FileHandle, ACCESS_MASK DesiredAccess,
POBJECT_ATTRIBUTES ObjectAttributes, ...,
ULONG Options) {
KPROCESSOR_MODE AccessMode;
if (Options & IO_NO_PARAMETER_CHECKING) {
AccessMode = KernelMode;
} else {
AccessMode = KeGetCurrentThread()->PreviousMode;
}
FILE_PARSE_CONTEXT ParseContext = {};
// Initialize other values
ParseContext->Options = Options;
return ObOpenObjectByName(
ObjectAttributes,
IoFileObjectType,
AccessMode,
NULL,
DesiredAccess,
&ParseContext,
&FileHandle);
}
Этот код показывает, что если указан INPC, то AccessMode для всех последующих вызовов устанавливается на KernelMode. Поэтому указание этой опции отключает не только MemAC, но и SecAC. Стоит отметить, что предыдущий режим потока не изменяется, только значение AccessMode, которое передается вперед в ObOpenObjectByName. IopCreateFile делегирует проверку указателя диспетчеру объектов, поэтому единственный способ добиться этого - отключить все проверки. Крайне важно, что IFAC не проверяется, он только передается внутри структуры контекста синтаксического анализа, с чем должен иметь дело остальной менеджер ввода-вывода.
Это еще не конец истории, также можно вызвать ZwCreateFile и передать специальный флаг OBJ_FORCE_ACCESS_CHECK (OFAC) внутри структуры OBJECT_ATTRIBUTES и обеспечить выполнение проверки доступа, даже если предыдущий режим доступа установлен на KernelMode. Поскольку вы не можете передать IFAC через ZwCreateFile, и в IopCreateFile не проверяется флаг OFAC, он должен быть в ObOpenObjectByName. На самом деле он немного глубже: сначала все параметры обрабатываются на основе AccessMode, переданного в ObOpenObjectByName, затем вызывается ObpLookupObjectName, который проверяет флаг OFAC, если он установлен, AccessMode принудительно возвращается в UserMode.
Теперь мы наконец можем понять, почему мы получили неожиданное поведение при открытии файла шрифта. Анализ символьной ссылки происходит внутри диспетчера объектов, а не диспетчера ввода-вывода, поэтому он не знает флаг IFAC. IopCreateFile сказал диспетчеру объектов выполнить все проверки, как если бы предыдущий режим доступа был KernelMode, поэтому это значение передается в ObpParseSymbolicLink, которое передается в RtlIsSandboxToken, что справедливо указывает на то, что он не запущен в изолированном процессе. Однако, как только файл действительно открывается, срабатывает флаг IFAC и гарантирует, что SecAC по-прежнему применяется к файлу. Если бы вызывающий код также указал OFAC, тогда символическая ссылка работала бы, поскольку синтаксический анализ происходил бы во время операции поиска, которая была принудительно установлена в UserMode.
Это само по себе является интересным результатом, по сути, любая операция, которая вызывается во время операции анализа диспетчера объектов, которая доверяет значению AccessMode, отключит проверки безопасности, если не указано OFAC. Однако это не та ошибка, о которой идет речь в этом блоге, для этого нам нужно углубиться, чтобы понять, как IFAC работает внутри IO Manager.
Разбор устройства ввода-вывода
Ответственность диспетчера объектов за открытие файла заканчивается, когда он находит именованный объект устройства в пространстве имен диспетчера объектов. Диспетчер объектов ищет функцию синтаксического анализа для типа устройства, которым является IopParseDevice, а затем передает всю информацию, о которой он знает. Это включает значение AccessMode, которое, как мы знаем, установлено на KernelMode, оставшийся путь для синтаксического анализа и буфер контекста синтаксического анализа, который включает параметр Options. Функция IopParseDevice выполняет некоторые собственные проверки безопасности, такие как проверка обхода устройства, выделяет новый пакет запроса ввода-вывода (IRP) и вызывает драйвер, ответственный за объект устройства.
Структура IRP содержит поле RequestorMode, которое отражает AccessMode файловой операции. Причина наличия поля RequestorMode заключается в том, что IRP может отправляться асинхронно. Поток, который обрабатывает операцию ввода-вывода, может не быть потоком, запустившим операцию ввода-вывода. Теперь вы можете догадаться, что именно здесь в игру вступает IFAC, возможно, менеджер ввода-вывода устанавливает RequestorMode в UserMode? Если вы действительно проверите это в драйвере ядра при доступе из IoCreateFile с помощью INPC, вы обнаружите, что в этом поле все еще установлено значение KernelMode, так что это не ответ.
Тип выполняемой операции и конкретные параметры операции передаются в структуре местоположения стека ввода-вывода, которая расположена сразу после структуры IRP. В случае открытия файла основным типом операции является IRP_MJ_CREATE, и используется поле Create структуры IO_STACK_LOCATION. Здесь на помощь приходит IFAC: если флаг указан для IopCreateFile, тогда в параметре Flags местоположения стека ввода-вывода будет установлен новый флаг SL_FORCE_ACCESS_CHECK (SFAC). Драйвер файловой системы должен убедиться, что он проверяет этот флаг, и не полагаться на то, что RequestorMode установлен в UserMode. Драйвер NTFS знает об этом и имеет следующий код:
C:
KPROCESSOR_MODE NtfsEffectiveMode(PIRP Irp) {
PIO_STACK_LOCATION loc = IoGetCurrentIrpStackLocation(Irp);
if (loc->MajorOperation == IRP_MJ_CREATE
&& loc->Flags & SL_FORCE_ACCESS_CHECK) {
return UserMode;
}
else {
return Irp->RequestorMode;
}
}
NtfsEffectiveMode вызывается любой операцией, которая будет выполнять функцию, связанную с безопасностью. Это гарантирует, что SecAC по-прежнему выполняется, даже если вызывающий объект находился в режиме ядра, пока был передан флаг IFAC. Драйвер файловой системы NTFS является фундаментальной частью ОС Windows и тесно взаимодействует с диспетчером ввода-вывода, поэтому неудивительно, что он знает, как поступать правильно. Однако в Windows все драйверы являются драйверами файловой системы, даже если они явно не реализуют файловую систему.
Я подумал, что было бы интересно узнать, сколько драйверов Microsoft и сторонних производителей действительно выполнили правильные проверки, или все они просто доверяли RequestorMode и основывали свои решения по безопасности на нем?
Определение класса ошибки
Наконец, мы должны определить класс ошибки. Для существования уязвимости, связанной с повышением привилегий, необходимо наличие двух отдельных компонентов.
- Инициатор режима ядра (код, вызывающий IoCreateFile или IoCreateFileEx), который устанавливает флаги INPC и IFAC, но не устанавливает OFAC. Это может быть драйвер или само ядро.
- Уязвимый Receiver, который использует RequestorMode во время обработки IRP_MJ_CREATE для принятия решения о безопасности, но не проверяет также флаги для SFAC.
В следующей таблице приводится сводка API, когда предыдущий режим вызывающего потока установлен на UserMode. Таблица включает в себя состояние опций ввода, INPC, IFAC и OFAC, а также соответствующий RequestorMode IRP и флаг SFAC. Я выделил, когда вызовы являются полезным инициатором.
Стоит отметить, что любые вызовы, в которые не передается IFAC, могут быть уязвимы для уязвимости привилегированного доступа к файлам, поскольку без сгенерированного флага SFAC даже NTFS не будет выполнять проверки безопасности. Есть и другие функции, похожие на IoCreateFile, такие как FltCreateFileEx, которые используются в особых случаях, но все они имеют похожие свойства. Также обратите внимание, что в таблице правила для IoCreateFileEx немного отличаются. Хотя это не задокументировано, IoCreateFileEx всегда передает параметр INPC в IopCreateFile, поэтому, если не указан флаг OFAC, он всегда будет выполнять свои операции с предыдущим режимом доступа, установленным на KernelMode.
Идеальный инициатор - это тот, который открывает произвольный путь от пользователя и предоставляет полный контроль над всеми параметрами IoCreateFile, а дескриптор открытого файла возвращается обратно в пользовательский режим. Однако в зависимости от приемника полный контроль может не требоваться.
Получатель может выполнить ряд действий при получении IRP. Общим для драйверов файловой системы является анализ оставшегося имени файла и выполнение на его основе некоторых дальнейших действий, таких как открытие другого файла. Другая возможность - разобрать блок расширенных атрибутов (EA) и выполнить какое-то действие на его основе. Возможно, открытие объекта устройства обычно требует проверки доступа, которую обходит установка RequestorMode на KernelMode.
Примеры
Вот несколько примеров, которые я нашел как инициаторов, так и получателей. Все это основано на коде, поставляемом с Windows 10 1709, что на две версии ниже того, что доступно сегодня (1809), но многие из этих примеров все еще существуют в последних версиях Windows, а также в Windows 7 и 8. Все примеры представляют собой код Microsoft, поэтому сторонний разработчик, вероятно, еще меньше понимает поведение системы.
Чтобы найти эти примеры, я не использовал никаких специальных инструментов статического анализа, вместо этого я просто искал их вручную. Я оставил более глубокое расследование Microsoft.
Получатели
Поиск получателей может быть намного сложнее, чем инициаторов, поскольку нет импортированной функции для поиска, которая дает четкий сигнал для поиска. Вместо этого я искал драйверы, которые импортировали IoCreateDevice, чтобы убедиться, что драйвер предоставляет какое-либо устройство. Затем я отфильтровал импортированные драйверы API-интерфейсы на те, которые принимали явный параметр AccessMode, например SeAccessCheck или ObReferenceObjectByHandle. Конечно, это не сильно ограничивало количество драйверов, поэтому в конечном итоге мне пришлось вручную проанализировать драйверы, которые выглядели наиболее интересными. В моем анализе "настоящие" драйверы файловой системы, такие как NTFS и FAT, всегда кажутся правильными.
WS2IFSL
Хотя этот драйвер не всегда включен, он используется для создания файлового объекта, который передает запросы на чтение /запись в приложение пользовательского режима с помощью APC. При создании нового объекта вы можете указать EA с информацией для создания файла Socket или Process. APC настраивается в функции CreateProcessFile на основе информации в EA. Драйвер использует RequestorMode без каких-либо дополнительных проверок, это позволит APC обратного вызова выполняться в режиме ядра. При создании файла процесса вы также передаете дескриптор потоку, который открывается для доступа THREAD_SET_CONTEXT для использования с APC. Установка KernelMode позволяет использовать дескрипторы ядра при вызове ObReferenceObjectByHandle, однако, поскольку поток должен находиться в вызывающем процессе, это не является большим преимуществом.
C:
NTSTATUS DispatchCreate(DEVICE_OBJECT* DeviceObject, PIRP Irp) {
PFILE_FULL_EA_INFORMATION ea = Irp->AssociatedIrp.SystemBuffer;
PIO_STACK_LOCATION loc = IoGetCurrentIrpStackLocation(Irp);
if (ea->EaNameLength != 7)
return STATUS_INVALID_PARAMETER;
if (!memcmp(ea->EaName, "NifsSct", 8))
return CreateSocketFile(lock->FileObject, Irp->RequestorMode, ea);
if (!memcmp(ea->EaName, "NifsPvd", 8))
return CreateProcessFile(lock->FileObject, Irp->RequestorMode, ea);
// ...
}
Чтобы эксплуатировать его, совместимый инициатор должен иметь возможность предоставить EA для файла процесса. Затем необходимо создать файл сокета, который ссылается на этот файл процесса, и выполнить операцию чтения/записи, чтобы заставить APC выполнить. В современных версиях Windows 10 вам также придется беспокоиться о SMEP и Kernel CFG. Если вы укажете подпрограмме APC на адрес пользовательского режима, ядро будет проверять ошибки, когда оно переходит на выполнение APC в режиме KernelMode, как показано на следующем снимке экрана, который я создал с помощью специального инициатора для настройки WS2IFSL.
NPFS
Хотя NPFS правильно проверяет SFAC, он использует RequestorMode, чтобы определить, разрешено ли вызывающему коду указать произвольный блок EA. Обычно при открытии именованного канала драйвер записывает вызывающий PID и идентификатор сеанса. Затем эта информация может быть предоставлена через API, такие как GetNamedPipeClientProcessId. Если вызывающий код является UserMode, то коду не разрешено устанавливать блок EA, но если это KernelMode, можно использовать произвольный блок EA, что означает, что поля PID и идентификатора сеанса могут быть подделаны.
C:
NTSTATUS NpCreateClientEnd(PIRP Irp, ...) {
// ...
PFILE_FULL_EA_INFORMATION ea = Irp->AssociatedIrp.SystemBuffer;
PVOID Data;
SIZE_T Length;
if (!NpLocateEa(ea, "ClientComputerName", &Data, Length))
return STATUS_INVALID_PARAMETER;
if (!IsValidEaString(Data, Length) || Irp->RequestorMode != KernelMode)
return STATUS_INVALID_PARAMETER;
NpSetAttributeInList(Irp, CLIENT_COMPUTER_NAME, Data, Length);
NpLocateEa(ea, "ClientProcessId", Data, Length);
NpSetAttributeInList(Irp, CLIENT_PROCESS_ID, Data, Length);
NpLocateEa(ea, "ClientSessionId", Data, Length);
NpSetAttributeInList(Irp, CLIENT_SESSION_ID, Data, Length);
// ...
}
Такое поведение используется, чтобы позволить драйверу SMB установить поле имени компьютера и идентификатор сеанса. Если какой-то сервис доверяет этой информации, ее можно использовать для повышения привилегий. Чтобы эксплуатировать это, вы должны иметь возможность установить произвольный EA как KernelMode, а чтобы сделать что-нибудь интересное, вам потребуется доступ к открытому дескриптору.
Инициаторы
Чтобы найти инициаторов, я просмотрел ядро и драйверы для любых функций, которые вызывали IoCreateFile и другие, и провел базовую проверку параметров вызова для флагов параметров и атрибутов объектов. С подходящими кандидатами я смог более внимательно изучить, на какие параметры может влиять пользователь. Поиск инициаторов относительно тривиален, если вы понимаете класс ошибки, поскольку вы можете быстро сузить цели, просто просматривая импортированные вызовы целевых методов.
Класс NTOSKRNL NtSetInformationFile FileRenameInformation
При переименовании файла вы можете указать произвольный путь, даже если файл не может находиться на томе, отличном от исходного. Функция IopOpenLinkOrRenameTarget вызывается, чтобы сначала открыть целевой путь, используя IoCreateFileEx, передающий INPC и обычно IFAC (он также устанавливает IO_OPEN_TARGET_DIRECTORY, но это не важно для операции). Этот инициатор позволяет указать только полный путь.
Драйвер сервера SMBv2
Серверы SMB будут открывать файлы на общем ресурсе с помощью IoCreateFileEx, например, в Smb2CreateFile. Он указывает IFAC, но не INPC, потому что вызов выполняется в системном потоке, поэтому предыдущий режим доступа уже является KernelMode. Обычно невозможно перенаправить создание файла на произвольный путь диспетчера объектов NT, поскольку сервер передает относительный путь к дескриптору открытого тома. Хотя вы можете добавить точку монтирования в каталог в файловой системе и получить доступ к серверу локально, ядро намеренно ограничивает целевое устройство ограниченным набором типов.
C:
if (ParseContext->ReparseTag == IO_REPARSE_TAG_MOUNT_POINT) {
switch (ParseContext->TargetDevice){
case FILE_DEVICE_DISK:
case FILE_DEVICE_CD_ROM:
case FILE_DEVICE_DISK:
case FILE_DEVICE_TAPE:
break;
default:
return STATUS_IO_REPARSE_DATA_INVALID;
}
}
В реализации была ошибка, которая позволяет обойти проверку устройства, которая позволяет использовать локальную точку монтирования для перенаправления сервера SMB для открытия любого файла устройства. Драйвер SMBv2 предъявляет особые требования к символическим ссылкам NTFS: он должен возвращать информацию о ссылках клиенту для обработки. Для поддержки символьной ссылки сервер передает флаг параметров IO_STOP_ON_SYMLINK, как показано ниже.
C:
NTSTATUS Smb2CreateFile(HANDLE VolumeHandle, PUNICODE_STRING Name, ...) {
// ...
int ReparseCount = 0;
OBJECT_ATTRIBUTES ObjectAttributes;
ObjectAttributes.RootDirectory = VolumeHandle;
ObjectAttributes.ObjectName = Name;
IO_STATUS_BLOCK IoStatus = {};
do {
status = IoCreateFileEx(
&FileHandle,
DesiredAccess,
&ObjectAttributes,
&IoStatus,
...
IO_STOP_ON_SYMLINK | IO_FORCE_ACCESS_CHECK
);
if (status == STATUS_STOPPED_ON_SYMLINK) {
UNICODE_STRING NewName;
status = SrvGraftName(ObjectAttributes.ObjectName,
(PREPARSE_DATA_BUFFER)IoStatus.Information, &NewName);
if (status == STATUS_STOPPED_ON_SYMLINK)
break;
ObjectAttributes.RootDirectory = NULL;
ObjectAttributes.ObjectName = NewName;
continue;
}
} while(ReparseCount++ < MAXIMUM_REPARSE_COUNT);
// ...
}
Если IoCreateFileEx возвращает STATUS_STOPPED_ON_SYMLINK, сервер извлекает возвращенную структуру REPARSE_DATA_BUFFER из IO_STATUS_BLOCK и передает ее служебной функции SrvGraftName. Буфер повторной обработки может быть либо точкой монтирования, либо символической ссылкой NTFS. Если это символическая ссылка, то SrvGraftName снова возвращает STATUS_STOPPED_ON_SYMLINK, что позволяет серверу вернуть буфер вызывающему коду. Если буфер повторного анализа является точкой монтирования, то SrvGraftName строит новый абсолютный путь только на основе строки, найденной в REPARSE_DATA_BUFFER, он не проверяет целевое устройство. Сервер повторно отправляет открытый запрос с новым абсолютным путем, который теперь может указывать на любое устройство в системе.
Этот инициатор был лучшим, что я нашел во время своего анализа. Вы можете указать почти все аргументы IoCreateFileEx, включая буфер EA. Это позволяет вам инициализировать драйвер, для которого требуется EA (например, WS2IFSL), который открывает гораздо больше возможностей для атак. Поскольку он выполняется в системном потоке, RequestorMode и предыдущий режим потока устанавливаются на KernelMode, что может привести к другой интересной поверхности для атаки.
Впечатляет, что это не идеальный инициатор, но открытое устройство должно поддерживать определенные допустимые IRP, такие как IRP_MJ_GET_INFORMATION_FILE, в противном случае сервер не вернет действительный дескриптор вызывающему коду. Без этой проверки было бы тривиально использовать WS2IFSL, поскольку вы могли бы выполнить операцию чтения/записи, чтобы заставить APC выполняться в режиме ядра. Даже если вы можете вернуть дескриптор файла, сервер SMB намеренно ограничивает коды управления вводом-выводом, которые вы можете отправлять, что ограничивает многие интересные вещи, которые вы могли бы сделать с этой уязвимостью. Несмотря на это, я посчитал это серьезной проблемой, поэтому сообщил об этом непосредственно в MSRC. Баг был исправлен как CVE-2018-0749 с использованием специального параметра Extra Creation Parameter, который отфильтровывает все другие точки повторной обработки, кроме символических ссылок.
Следующие шаги
Хотя я считал, что это серьезный класс ошибок, оказалось, что найти подходящую пару инициатор/получатель было очень сложно. В своем исследовании я не нашел ни одной пары, которая дала бы прямую эскалацию привилегий. Лучшая пара, которую я обнаружил, - это сочетание инициатора SMBv2 с подделкой идентификатора процесса NPFS. Хотя мне не удалось идентифицировать службу, которая будет использовать PID клиента для какой-либо операции, связанной с безопасностью, вполне возможно, что существует сторонняя служба.
Я мог бы снова добавить эту проблему в свой список интересных или неожиданных действий, чтобы посмотреть, смогу ли я использовать ее позже. Но вместо этого я решил поговорить со своими контактами в MSRC, чтобы узнать, можем ли мы сотрудничать. Вместе с MSRC я написал документ, объясняющий класс ошибки и описывающий некоторые из моих выводов. В то же время я сообщил о проблеме с сервером SMB по обычным каналам, поскольку это была самая серьезная проблема, которую я обнаружил. Это привело к встречам с различными командами на Bluehat 2017 в Редмонде, где был сформирован план для Microsoft использовать доступ к исходному коду, чтобы обнаружить степень этого класса ошибок в ядре Windows и базе кода драйверов. Обратите внимание, у меня не было доступа к исходному коду, эта часть расследования была делегирована MSRC, результаты которого опубликованы в их блоге.
Стоит отметить, что хотя я применил стандартный 90-дневный крайний срок раскрытия к отчету, я не применил явный крайний срок к отчету о классе ошибок. Без единой ошибки, на которую можно было бы указать, было бы сложно и, вероятно, непродуктивно обеспечить соблюдение такого срока. Однако я убедился, что MSRC согласилась опубликовать технические подробности по этому вопросу независимо от результата. Вот почему мы пишем об этом сейчас, через 12 месяцев после предоставления отчета.
Заключение
Всегда интересно найти новый класс ошибок в Windows, за которым можно будет охотиться.
По счастливой случайности все обернулось не так серьезно, как могло бы быть. Хотя было бы разумно указать два отдельных режима доступа, один для MemAC и один для SecAC, это не то, что использовалось в исходной конструкции NT. Для обратной совместимости маловероятно, что поведение будет изменено. Этот класс ошибок связан как с плохой документацией, так и с техническими проблемами, поскольку, хотя поведение нельзя изменить, оно также плохо документировано.
Если вы посмотрите документацию по IoCreateFile, вы увидите новое примечание:
"Для запросов на создание, исходящих из пользовательского режима, если драйвер устанавливает и IO_NO_PARAMETER_CHECKING, и IO_FORCE_ACCESS_CHECK в параметре Options IoCreateFile, тогда он также должен установить OBJ_FORCE_ACCESS_CHECK в параметре ObjectAttributes.
Для получения информации об этом флаге смотри член в OBJECT_ATTRIBUTES".
Это замечание было добавлено совсем недавно. В большинстве документации для разработчиков Microsoft на GitHub вы даже можете найти этот коммит.
Вполне вероятно, что любые ошибки, обнаруженные MSRC, будут исправлены только в последних версиях Windows 10, поэтому, если вы разработчик, вам следует прочитать сообщение в блоге Microsoft, чтобы понять, как избежать этих проблем в ваших драйверах, а также способы обнаружения этимх различные проблемы в вашей кодовой базе. Исследователям в области безопасности следует помнить о другом, когда вы рассматриваете новый драйвер ядра Windows.
Я хотел бы поблагодарить Стивена Хантера и Гэвина Томаса из MSRC, которые были моими основными контактными лицами для исправления этого класса ошибок.
Источник: https://googleprojectzero.blogspot.com/2019/03/windows-kernel-logic-bug-class-access.html
Автор перевода: yashechka
Переведено специально для https://xss.pro