Наконец, после долгих лет обсуждений, в Windows 10 (сборка 1903) вышла очень интересная вещь - часть реализации технологии Intel "Control-flow Enforcement Technology" (CET) (https://software.intel.com/sites/de...ntrol-flow-enforcement-technology-preview.pdf). Более подробная информация об этой технологии добавляется в каждый релиз Windows, и в этом году, релиз 20H1 (сборка 2004 г.), завершается поддержкой возможностей Пользовательского Режима Теневого Стека CET, которые будут доступны в процессорах Intel Tiger Lake.
Напомним, что Intel CET - это аппаратное средство защиты, которое устраняет два типа нарушений целостности потока управления, обычно используемых эксплоитами: нарушение forward-edge (косвенные инструкции CALL и JMP) и нарушение backward-edge (инструкции RET).
В то время как реализация forward-edge менее интересна (так как это по существу более слабая форма clang-cfi (https://clang.llvm.org/docs/ControlFlowIntegrity.html), похожая на Microsoft Control Flow Guard (https://docs.microsoft.com/en-us/windows/win32/secbp/control-flow-guard), реализация backward-edge опирается на фундаментальное изменение в ISA: введение нового стека называемого "Теневой Стек", который теперь реплицирует адреса возврата, которые помещаются в стек инструкцией CALL, с инструкцией RET, теперь проверяющей значения как стека, так и теневого стека, и генерирующей прерывание INT #21 (Ошибка Защиты Потока Управления) в случай несоответствия.
Поскольку операционные системы и компиляторы должны иногда поддерживать последовательности потоков управления, отличные от инструкций CALL/RET (такие как раскрутка исключений и механизм longjmp (https://en.cppreference.com/w/cpp/utility/program/longjmp)), иногда необходимо манипулировать "Указателем Теневого Стека" (SSP) на уровне системы, чтобы соответствовать требуемому поведению - и в свою очередь, проверять, чтобы сама эта манипуляция не стала потенциальным обходом. В этом посте мы расскажем, как Windows добивается этого.
Прежде чем углубляться в то, как Windows манипулирует и проверяет теневой стек для потоков, необходимо разобраться с двумя частями его реализации. Первое - это фактическое местонахождение и разрешения SSP, а второе - это механизм, используемый для хранения/восстановления SSP при переключении контекста между потоками, а также способы внесения изменений в SSP при необходимости (например, во время раскрутки исключения).
Чтобы объяснить эти механизмы, нам нужно углубиться в функцию центрального процессора Intel, которая была первоначально введена Intel для поддержки инструкций "Advanced Vector eXtensions" (AVX) (https://en.wikipedia.org/wiki/Advanced_Vector_Extensions) и впервые поддержана Microsoft в операционной системе Windows 7. А поскольку добавление поддержки для этой функции потребовало масштабной реструктуризации структуры CONTEXT в недокументированную структуру CONTEXT_EX (и добавление документированных и нативных функций для ее манипулирования), нам также придется поговорить об ее внутренностях!
Наконец, нам даже придется пройтись по некоторым внутренним компонентам форматов файлов компилятора и PE, а также новым классам информации о процессах, чтобы охватить дополнительные тонкости и требования к функциональности CET в Windows. Мы надеемся, что приведенное ниже оглавление поможет вам разобраться в этом подробном описании этих возможностей. Кроме того, при необходимости можно получить аннотированный исходный код для различных недавно представленных функций, щелкнув имена функций на основе нашего связанного репозитория GitHub (https://github.com/yardenshafir/cet-research).
Оглавление
Внутренности XState
Процессоры класса архитектуры x86-x64 изначально начинались с простого набора регистров, с которыми знакомы большинство исследователей безопасности - регистры общего назначения (RAX, RCX), регистры управления (например, RIP/RSP), регистры с плавающей запятой (XMM, YMM, ZMM) и некоторые регистры управления, отладки и тестирования. Однако по мере добавления дополнительных возможностей процессора необходимо было определить новые регистры, а также регистры конкретного состояние процессора, связанные с этими возможностями. А поскольку многие из этих функций являются локальными для потока, они должны быть сохранены и восстановлены во время переключения контекста.
В ответ, Intel определила спецификацию (https://www.intel.com/content/dam/w...-architectures-software-developers-manual.pdf) "eXtended State" (XState), которая связывает различные состояния процессора с битами в "Маске Состояний" и вводит дополнительные инструкции, такие как XSAVE и XRSTOR, для чтения и записи запрошенных состояний из "Области XSAVE". Поскольку эта область в настоящее время является критически важной частью хранилища регистров CET для каждого потока, и большинство людей в значительной степени игнорируют поддержку XSAVE из-за ее изначального внимания к функциям с плавающей запятой, AVX и "Расширения Защиты Памяти - Memory Protection eXtensions" (MPX) (https://software.intel.com/en-us/articles/introduction-to-intel-memory-protection-extensions), мы подумали, что обзор функциональности и расположение памяти будет полезно для читателей.
Область XSAVE
Как уже упоминалось, область XSAVE первоначально использовалась для хранения некоторых новых функциональных возможностей с плавающей запятой, таких как AVX, которые были добавлены в процессоры Intel, и для консолидации существующих состояний регистров x87 FPU и SSE, которые ранее сохранялись с помощью инструкций FXSTOR и FXRSTR. Эти первые два унаследованных состояния были определены как часть "Устаревшей области XSAVE", а любые дополнительные регистры процессора (такие как AVX) были добавлены в "Расширенную область XSAVE". Между ними "Заголовок области XSAVE" используется для описания того, какие расширенные функции присутствуют через маску состояния, называемую XSTATE_BV.
В то же время, был добавлен новый "Расширенный Регистр Управления - eXtended Control Register" (XCR0), который определяет, какие состояния поддерживаются операционной системой как часть функциональности XSAVE, а инструкции XGETBV и XSETBV были добавлены для настройки регистра XCR0 (и, возможно, в будущем XCR также). Например, операционные системы могут выбрать программирование регистра XCR0 так, чтобы он не содержало биты состояния функции для регистров x87 FPU и SSE, что означает, что они будут сохранять эту информацию вручную с устаревшими инструкциями FXSTOR и сохранят только расширенное состояние функции в своих областях XSAVE.
По мере роста количества расширенных наборов и возможностей регистров, таких как "Ключи Защиты Памяти - Memory Protection Keys-https://www.kernel.org/doc/html/latest/core-api/protection-keys.html" (MPK), в которые добавлено "Состояние Пользовательского Регистра Ключей Защиты - Protection Key Register User State" (PKRU), новые процессоры вводили различие между "Состоянием Супервизора", которое может только быть измененным кодом CPL0 с использованием инструкций XSAVES и XRSRTORS, а также версий "сжатия" и "оптимизации" (XSAVEC/XSAVEOPT), чтобы усложнить ситуацию в типичном для Intel стиле. Новый "Специфичный Регистр Модели - Model Specific Register" (MSR), названный IA32_XSS, был добавлен, чтобы определить, какие состояния доступны только для супервизора.
Механизм "оптимизированного XSAVE" существует, чтобы гарантировать, что только состояние процессора, которое фактически было изменено другим потоком с момента последнего переключения контекста (если есть), будет фактически записано в области XSAVE. Для отслеживания этой информации существует внутренний регистр процессора XINUSE. Когда используется XSAVEOPT, маска XSTATE_BV теперь включает только биты, соответствующие состояниям, которые были фактически сохранены, а не просто биты всех запрошенных состояний.
Механизм "сжатого XSAVE", с другой стороны, исправил расточительный недостаток в дизайне XState: по мере добавления все более и более расширенных функций, таких как AVX512 и "Intel Processor Trace (https://software.intel.com/en-us/blogs/2013/09/18/processor-tracing)" (IPT), это означало, что даже для потоков, которые не использовали эти возможности, достаточно большая область XSAVE должна была быть выделена и записана (заполнена нулями) процессором. Хотя оптимизированный XSAVE позволит избежать этих записей, это все равно означает, что любые расширенные функции, следующие за большими, но еще неиспользованными состояниями, будут находиться на больших смещениях от основного буфера области XSAVE.
В XSAVEC эта проблема решается путем использования только пространства для сохранения функций XState, которые фактически включены (и используются, поскольку сжатие подразумевает оптимизацию) текущим потоком, и последовательно размещают каждое сохраненное состояние в памяти без пропусков между ними. (но потенциально с фиксированным 64-байтовым выравниванием, которое предоставляется как часть "маски выравнивания" через инструкцию CPUID). Заголовок области XSAVE, показанный ранее, теперь расширен второй маской состояния, называемой XCOMP_BV, которая указывает, какие из запрошенных битов состояния, которые были запрошены, могут присутствовать в вычисляемой области. Обратите внимание, что в отличие от XSTATE_BV, эта маска не пропускает биты состояния, которые не были частью XINUSE - она включает в себя все возможные биты, которые могли быть сжаты - все равно необходимо проверить XSTATE_BV, чтобы определить, какие области состояния действительно присутствуют. Наконец, бит 63 всегда устанавливается в XCOMP_BV, когда использовалась сжатая инструкция, как индикатор того, для какого формата имеет область XSAVE.
Таким образом, использование сжатого и не сжатого формата определяет внутреннюю разметку и размер области XSAVE. Сжатый формат будет выделять память в области XSAVE только для функций процессора, используемых потоком, в то время как несжатый будет выделять память для всех функций процессора, поддерживаемых процессором, но заполнять только те, которые используются потоком. На диаграмме ниже показан пример того, как будет выглядеть область XSAVE для того же потока, но при использовании одного и другого формата.
Подводя итог, в котором говорится, что семейство инструкций XSAVE * / XRSTOR * будет работать с комбинацией:
Как только эти биты маскируются вместе, окончательный набор результирующих битов состояния записывается командой XSAVE в заголовок области XSAVE в поле, называемом XSTATE_BV. В случае, когда используется "сжатый XSAVE", результирующие биты состояния, пропускающие пункт 4 (XINUSE), записываются в заголовок области XSAVE в поле XCOMP_BV. На схеме ниже показаны полученные маски.
Конфигурация Xstate
Поскольку каждый процессор имеет свой собственный набор функций, потенциальных размеров, возможностей и механизмов с поддержкой XState, Intel предоставляет всю эту информацию через различные классы инструкции CPUID, которые операционная система должна запрашивать при работе с XState. Windows выполняет эти запросы при загрузке и сохраняет информацию в структуре XSTATE_CONFIGURATION, которая показана ниже (документировано в файле Winnt.h)
После заполнения этих данных, ядро сохраняет эту информацию в структуре KUSER_SHARED_DATA, к которой можно получить доступ через переменную SharedUserData и расположенную по адресу 0x7FFE0000 на всех платформах Windows.
Например, вот выходные данные нашей тестовой системы версии 19H1, которая поддерживает как оптимизированные, так и сжатые формы области XSAVE и имеет функциональные биты x87 FPU(0), SSE(1), AVX(2) и MPX(3, 4) включенными.
В массиве Features можно найти размер и смещение каждой из этих пяти функций:
Сложение этих размеров дает нам 0x3C0, что является значением, показанным выше в поле FeatureSize. Однако обратите внимание, что, поскольку эта система поддерживает функцию Compacted XSAVE, показанные здесь смещения не имеют значения, и только поле AllFeatures полезно для ядра, которое содержит размер каждой функции, но не смещение (как это будет определяется на основе маски сжатия, используемой в XCOMP_BV).
Политика XState
К сожалению, несмотря на то, что процессор может претендовать на поддержку определенной функции XState, часто оказывается, что из-за различных аппаратных ошибок некоторые конкретные процессоры могут не полностью или неправильно поддерживать эту функцию в конце концов. Для обработки этой возможности Windows использует политику XState, которая представляет собой информацию, хранящуюся в разделе ресурсов Драйвера Политики Оборудования, который обычно называется HwPolicy.sys.
Поскольку архитектура Intel x86 представляет собой комбинацию множества поставщиков процессоров, конкурирующих с вариантами наборов функций друг друга, ядро должно проанализировать политику XState и сравнить текущую строку поставщика и Версию Микрокода, а также его Подпись, Функции и Расширенные функции. (а именно, регистры RAX, RDX и RCX из запроса инструкции CPUID 01h)
Эта работа выполняется при загрузке функцией KiIntersectFeaturesWithPolicy, которая вызывается функцию KiInitializeXSave, которая вызывает KiLoadPolicyFromImage для загрузки соответствующей политики XState, вызывает KiGetProcessorInformation для получения данных центрального процессора, упомянутых ранее, а затем проверяет каждый бит функции, включенный в настоящее время в конфигурации XState, посредством обращений к KiIsXSaveFeatureAllowed.
Эти функции работают с ресурсом 101 в драйвере HwPolicy.sys, который начинается со следующей структуры данных:
Например, в нашей системе версии 19H1 содержимое (которое мы извлекли с помощью Resource Hacker) было следующим:
Для каждого XSAVE_FEATURE находится смещение в структуре XSAVE_VENDORS, которое содержит массив структур XSAVE_VENDOR, каждая из которых имеет строку поставщика центрального процессора (на данный момент каждая из них выглядит как "GenuineIntel", "AuthenticAMD" или "CentaurHauls"), и смещение к структуре XSAVE_CPU_ERRATA. Например, наша тестовая система версии 19H1 имела следующую информацию для функции 0:
Наконец, каждая структура XSAVE_CPU_ERRATA содержит соответствующие информационные данные процессора, которые соответствуют известным ошибкам, которые препятствуют поддержке указанной функции XState. Например, в нашей тестовой системе первые ошибки из смещения выше были:
Инструмент, который дампит политику аппаратного обеспечения вашей системы для всех функций XState, доступен на нашем GitHub (https://github.com/yardenshafir/cet-research/tree/master/Xpolicy) здесь. На данный момент во всей политике отображается только одна ошибка (показанная выше).
Наконец, следующие дополнительные параметры командной строки загрузчика (и соответствующие параметры BCD) могут использоваться для дальнейшей настройки возможностей Xstate:
CET XSAVE Формат области
В рамках реализации CET корпорация Intel определила два новых бита в стандарте XState, которые называются XSTATE_CET_U (11) и XSTATE_CET_S (12), что соответствует состоянию пользователя и супервизора соответственно. Первое состояние представляет собой 16-байтовую структуру данных, которая MSDN документирует как XSAVE_CET_U_FORMAT, содержащая MSR IA32_U_CET (где настроен флаг "Включение Теневого Стека") и MSR IA32_PL3_SSP (где хранится "SSP уровня привилегий 3"). Второй, который еще не имеет определения MSDN, включает MSR IA32_PL0/1/2_SSP.
Как следует из имен полей, "регистры", связанные с CET, на самом деле являются значениями, хранящимися в соответствующих MSR, к которым обычно можно получить доступ только через привилегированные инструкции RDMSR и WRMSR в кольце защиты 0. Однако, в отличие от большинства MSR, в которых хранятся глобальные данные процессора, CET можно включить для каждого потока, а указатель теневого стека также явно для каждого потока. По этим причинам данные, связанные с CET, должны стать частью функциональности XState, чтобы операционные системы могли правильно обрабатывать переключатели потоков.
Поскольку регистры CET в основном являются регистрами MSR, которые обычно могут быть изменены только кодом ядра, они не доступны через инструкции CPL3 XSAVE/XRSTOR, и их соответствующие биты состояния всегда устанавливаются в 1 в MSR IA32_XSS. Однако, что усложняет ситуацию, так это то, что операционная система не может полностью заблокировать код пользовательского режима от изменения SSP. Код пользовательского режима может на законных основаниях нуждаться в обновлении SSP как части обработки исключений, раскрутки, setjmp/longjmp или определенных функций, таких как механизм "Нитей" в Windows.
Таким образом, операционные системы должны предоставлять возможность потокам изменять состояние CET в XState посредством системного вызова, подобно тому, как Windows предоставляет функция SetThreadContext как механизм обновления определенных защищенных регистров центрального процессора , таких как CS и DR7, при условии соблюдения определенных правил. Поэтому, в следующем разделе мы увидим, как структура CONTEXT превратилась в структуру CONTEXT_EX в более современных версиях Windows для поддержки информации, связанной с XState, и как должна была быть добавлена специфичная для CET обработка для законных сценариев, связанных с исключениями, в то же время избегая злонамеренных атак потока управления через поврежденные контексты.
Внутреннее устройство структуры CONTEXT_EX
Для поддержки растущего числа регистров, которые должны быть сохранены при каждом переключении контекста, новые версии Windows имеют структуру CONTEXT_EX в дополнение к устаревшей структуре CONTEXT. Это было необходимо из-за того, что CONTEXT является структурой фиксированного размера, в то время как XSAVE ввел потребность в данных о состоянии процессора динамического размера, которые зависят от потока, процессора и даже политики конфигурации компьютера.
Структура CONTEXT_EX
К сожалению, хотя в настоящее время используется во всех функциях обработки исключений ядра и пользовательского режима, структура CONTEXT_EX в значительной степени недокументирована, за исключением случайного выпуска некоторой информации в заголовочных файлах Windows 7 и некоторого справочного кода Intel (что может свидетельствовать о том, что на самом деле Intel ответственна за определение этой мерзости). Просто посмотрите на этот блок комментариев и скажите нам, можете ли вы что-нибудь понять:
Таким образом, хотя эти заголовки пытаются объяснить структуру CONTEXT_EX, текст достаточно тупой (и полон ошибок на английском языке), так что нам потребовалось несколько раундов, пока мы не смогли его визуализировать, и почувствовал, что диаграмма может быть полезна ,
Как показано на схеме, структура CONTEXT_EX всегда находится в конце структуры CONTEXT и имеет 3 поля типа CONTEXT_CHUNK с именами All, Legacy и XState. Каждый из них определяет смещение и длину данных, связанных с ними, и существуют различные макросы RTL_ для извлечения соответствующего указателя данных.
Поле Legacy относится к началу исходной структуры CONTEXT (хотя длина может быть меньше на процессорах x86, если CONTEXT_EXTENDED_REGISTERS не указан). Поле "All" также относится к началу исходной структуры CONTEXT, но его длина описывает совокупность всех данных, включая сам CONTEXT_EX и пространство заполнения/выравнивания, необходимое для области XSAVE. Наконец, поле XState относится к структуре XSAVE_AREA_HEADER (которая затем определяет маску состояния, какие биты состояния включены и, следовательно, чьи данные присутствуют) и длину всей области XSAVE. Из-за этого макета важно отметить, что "All" и "Legacy" будут иметь отрицательные смещения.
Поскольку вся эта математика сложна, библиотека Ntdll.dll экспортирует различные API-интерфейсы, чтобы упростить построение, чтение, копирование и другие манипуляции с различными данными, хранящимися в структуре CONTEXT_EX (некоторые, но не все, эти API-интерфейсы внутренне используются ядром Ntoskrnl, но ничто не экспортируется). В свою очередь, библиотека KernelBase.dll экспортирует документированные функции Win32, которые внутренне используют эти возможности.
Инициализация структуры CONTEXT_EX
Во-первых, вызовы должны выяснить, какой объем памяти выделить для хранения структуры CONTEXT_EX, что можно сделать с помощью следующего API:
Ожидается, что вызовы предоставят соответствующие флаги CONTEXT_XXX, чтобы указать, какие регистры они намереваются сохранить (а именно CONTEXT_XSTATE, в противном случае использование CONTEXT_EX на самом деле не приносит ничего). Эта функция затем читает SharedUserData.XState.EnabledFeatures и SharedUserData.XState.EnabledUserVisibleSupervisorFeatures и передает объединение всех битов в расширенную функцию (также экспортированную), показанную ниже.
Обратите внимание, что этот более новый API позволяет вручную указывать, какие состояния XState должны фактически сохраняться, вместо того, чтобы извлекать все включенные функции из конфигурации XState в общих пользовательских данных. Это приводит к тому, что структура CONTEXT_EX будет меньше и не будет содержать достаточно места для всех возможных Данных Состояния XState, поэтому при использовании этой структуры CONTEXT_EX в будущем не следует использовать Биты Состояния XState вне указанной маски.
Затем вызовы выделяет память для структуры CONTEXT_EX (в большинстве случаев Windows будет использовать функцию alloca(), чтобы избежать сбоев исчерпания памяти в путях исключений), и использует один из этих двух функций:
Как и прежде, новый API позволяет вручную указывать, какие состояния XState следует сохранять в их сжатой форме, в противном случае предполагается, что все доступные функции (основанные на SharedUserData) присутствуют. Очевидно, ожидается, что вызовы указывает те же флаги ContextFlags, что и при вызове RtlGetExtendedContextLength(2), чтобы убедиться, что структура контекста имеет правильный размер, как было выделено. В ответ вызовы теперь получает указатель на структуру CONTEXT_EX, которая, как ожидается, будет следовать за входным буфером CONTEXT.
Когда существует структуры CONTEXT_EX, вызов, скорее всего, сначала будет заинтересован в получении от него унаследованной структуры CONTEXT (без предположений о размерах), что можно сделать с помощью следующей функции:
Однако, как уже упоминалось выше, это недокументированные и внутренние API-интерфейсы, предоставляемые уровнем NT в Windows. Легитимные приложения Win32 вместо этого упростят использование XState-совместимых структур CONTEXT, используя вместо этого следующие функции:
Эти две функции ведут себя подобно комбинации использования недокументированных функций: когда вызовы сначала передают NULL в качестве параметров Buffer и Context, функция возвращает требуемую длину в ContextLength, которую вызывающие абоненты должны выделить из памяти. При второй попытке, вызовы передают выделенный указатель в Buffer и получают указатель на структуру CONTEXT в Context без какого-либо знания базовой структуры CONTEXT_EX.
Управление масками объектов XState в структутре CONTEXT_EX
Чтобы получить доступ к XSTATE_BV (маска расширенной функции), которая глубоко встроена в поле XSAVE_AREA_HEADER в структуре CONTEXT_EX, система экспортирует два API-интерфейса для простой проверки, какие функции XState включены в структуру CONTEXT_EX, с соответствующим функциями для изменение маски Xstate.
Однако обратите внимание, что Windows никогда не хранит состояния x87 FPU(0) и SSE(1) в области XSAVE и вместо этого использует инструкцию FXSAVE, что означает, что область XSAVE никогда не будет содержать Устаревшую Область, и немедленно начнется с XSAVE_AREA_HEADER. Благодаря этому Get API всегда будет маскировать 2 нижних бита. Кроме того, API-интерфейс Set также гарантирует, что указанная функция присутствует в EnabledFeatures Конфигурации Xstate.
Имейте в виду, что если в InitializeContext2 (или внутренних нативных API-интерфейсах) была задана жестко закодированная маска сжатия, то API-интерфейс Set не следует использовать, кроме как для исключения существующих битов состояния (поскольку добавление нового бита подразумевает дополнительный неинициализированный выход данные о состоянии границ в CONTEXT_EX, который уже был бы предварительно выделен без этих данных).
Документированная форма этих функций выглядит следующим образом:
Нахождение объектов XState в структуре CONTEXT_EX
Из-за сложности структуры CONTEXT_EX, а также того факта, что функции XState могут присутствовать либо в сжатом, либо в не в сжатом виде, и что их присутствие также зависит от различных масок состояний, описанных ранее (особенно, если поддерживается оптимизированный XSAVE ), вызовам нужна библиотечная функция, чтобы быстро и легко получить указатель на соответствующие данные о состоянии в области XSAVE в пределах структуры CONTEXT_EX.
В настоящее время существуют две такие функции, как показано ниже, причем функция RtlLocateExtendedFeature является просто оболочкой для функции RtlLocateExtendedFeature2, которая снабжает ее указателем на SharedUserData.XState в качестве параметра конфигурации. Поскольку оба экспортируются, вызовы могут также вручную указать свою собственную пользовательскую конфигурацию XState в последней функции, если они того пожелают.
Обе эти две функции получают структуру CONTEXT_EX и идентификатор запрашиваемой функции и анализируют данные конфигурации XState, чтобы получить указатель на место хранения функции в области XSAVE. Обратите внимание, что они не проверяют и не возвращают какое-либо фактическое значение для указанной функции, которая зависит от вызывающей стороны.
Чтобы найти указатель, функция RtlLocateExtendedFeature2 делает следующее:
Это означает, что для нахождения адреса определенной функции в несжатом формате достаточно проверить в SharedUserData, какие функции поддерживаются процессором. Однако в сжатом формате невозможно полагаться на смещения в SharedUserData, поэтому необходимо также проверить, какие функции включены в потоке, и рассчитать правильное смещение для объекта на основе размеров всех предыдущих функций.
В легитимных приложениях Win32 используются друие функции, который внутренне вызывает нативные функции выше, но с некоторой предварительной обработкой. Поскольку биты состояния 0 и 1 никогда не сохраняются как часть области XSAVE в структуре CONTEXT_EX, API-интерфейс Win32 обрабатывает эти два функциональных бита, извлекая их из соответствующих устаревших полей структуры CONTEXT, а именно FltSave для XSTATE_LEGACY_FLOATING_POINT и Xmm0 для XSTATE_LEGACY_SSE.
Пример использования
Чтобы разобраться с внутренностями XState, особенно в сочетании со структурой данных CONTEXT_EX, мы написали простую тестовую программу, доступную на нашем GitHub здесь (https://github.com/yardenshafir/cet-research/tree/master/ContextEx). Эта утилита демонстрирует некоторые из использованных функций, а также различные смещения, размеры и поведение. Вот выходные данные программы (которая использует регистры AVX) в системе с AVX, MPX и Intel PT:
Помимо прочего, обратите внимание, что прежний CONTEXT находится с отрицательным смещением, как и ожидалось, и как даже если система поддерживает состояние x87 FPU(1) и GSSE(2), XSAVEBV не содержит эти биты, поскольку они вместо этого сохранены в устаревшей области CONTEXT (и, следовательно, обратите внимание на отрицательные смещения связанных с ними данных состояния). После заголовка XSAVE, который составляет 0x40 байтов, обратите внимание, что состояние AVX(2) начинается со смещения 0x70, как и предполагалось в математике.
Проверка структуры CONTEXT_EX
Поскольку функции пользовательского режима могут создавать структуру CONTEXT_EX, которая в конечном итоге обрабатывается ядром и изменяет привилегированные части области XSAVE (а именно, данные состояния CET), Windows должна защищаться от нежелательных изменений, которые могут быть выполнены через функции, которые принимают CONTEXT_EX, такие как:
Поскольку любой из этих системных вызовов может заставить ядро изменить либо регистр MSR IA32_PL3_SSP, либо IA32_CET_U, а также напрямую изменить указатель RIP для неожиданной цели, Windows должна проверить, что переданная структура CONTEXT_EX не нарушает гарантии CET.
Вскоре мы расскажем, как это делается для проверки SSP в версии 19H1 и добавления проверки указателя RIP в версии 20H1. Сначала нужно было сделать небольшой рефакторинг, чтобы уменьшить вероятность неправильного использования функции NtContinue: введение функции NtContinueEx.
NtContinueEx и KCONTINUE_ARGUMENT
Как перечислено выше, функциональность функции NtContinue используется в ряде ситуаций, и для обеспечения устойчивости CET в лице функции, которая допускает произвольные изменения состояния процессора, в интерфейс необходимо было добавить более детальное управление. Это было сделано путем создания нового перечисления с именем KCONTINUE_TYPE, которое присутствует в структуре данных KCONTINUE_ARGUMENT, которая теперь должна быть передана в расширенную версию NtContinue — NtContinueEx.
Эта структура данных также содержит новое поле ContinueFlags, которое заменяет исходный аргумент TestAlert для NtContinue на флаг CONTINUE_FLAG_RAISE_ALERT (0x1), а также вводит новый флаг CONTINUE_FLAG_BYPASS_CONTEXT_COPY (0x2), который напрямую доставляет APC с новым APF с с новым TrapFrame. Это оптимизация, которая ранее была реализована путем проверки того, был ли указатель записи CONTEXT в определенном месте в стеке пользователя, что заставило функцию предполагать, что она использовалась как часть доставки APC пользовательского режима. Вызовы, желающие этого поведения, теперь должны явно установить флаг в ContinueFlags.
Обратите внимание, что хотя старый интерфейс по-прежнему поддерживается по устаревшим причинам, он внутренне вызывает функцию NtContinueEx, которая распознает входной параметр как параметр BOOLEAN TestAlert, а не KCONTINUE_ARGUMENT. Такой случай рассматривается как KCONTINUE_UNWIND для целей нового интерфейса.
В рамках этого рефактора существуют следующие четыре возможных типа:
Проверка Указателя Теневого Стека (SSP)
Как мы уже упоминали, существуют законные случаи, когда код пользовательского режима должен будет изменить указатель теневого стека, например, раскрутка исключений, APC, longjmp и так далее. Но операционная система должна проверить новое значение, запрошенное для SSP, чтобы для предотвращения обходов CET. В версии 19H1 это было реализовано новой функцией KeVerifyContextXStateCetU. Эта функция получает поток, контекст которого изменяется, и новый контекст для потока и выполняет следующие действия:
Любые сбои в описанных проверках будут возвращать STATUS_SET_CONTEXT_DENIED, тогда как STATUS_SUCCESS возвращается в других случаях.
Включение CET также неявно включает проверку содержимого стека, первоначально реализованную в Windows 8.1 вместе с CFG. Это видно через бит CheckStackExtents в поле ProcessFlags KPROCESS. Это означает, что всякий раз, когда проверяется целевой SSP, также вызывается KeVerifyContextRecord, и функция проверяет, является ли целевой RSP либо частью ограничений пользовательского стека TEB текущего потока (или ограничений пользовательского стека TEB32, если это процесс WOW64). Эти проверки, реализованные функцией RtlGuardIsValidStackPointer (и RtlGuardIsValidWow64StackPointer), ранее были задокументированы (и показаны как недостаточные) исследователями как в Tenable (https://medium.com/tenable-techblog/api-series-setthreadcontext-d08c9f84458d), так и в enSilo (https://blog.ensilo.com/atombombing-cfg-protected-processes).
Проверка указателя инструкций (RIP)
В сборке 19030 появилась другая функция, использующая Intel CET - проверка того, что новый указатель RIP, который пытается установить для процесса, является допустимым. Так же, как проверка SSP, эта защита может быть включена, только если для потока включен CET. Однако проверка указателя RIP не включена по умолчанию и должна быть включена для процесса (что указывается битом UserCetSetContextIpValidation в поле MitigationFlags2Values в EPROCESS).
При этом для текущих сборок создается впечатление, что при вызове CreateProcess и использовании атрибута PROC_THREAD_ATTRIBUTE_MITIGATION_POLICY, если флаг PROCESS_CREATION_MITIGATION_POLICY2_CET_USER_SHADOW_STACKS_ALWAYS_ON включен, эта опция будет установлена. (Обратите внимание, что вызов SetProcessMitgationPolicy со значением ProcessUserShadowStackPolicy недопустим, поскольку CET можно включить только во время создания процесса).
Интересно, однако, что новая опция безопасности была добавлена на карту защиты, PS_MITIGATION_OPTION_USER_CET_SET_CONTEXT_IP_VALIDATION (32). Переключение этого (недокументированного) параметра приводит к включению бита AuditUserCetSetContextIpValidation в поле MitigationFlags2Values, которое будет вскоре описано. Кроме того, поскольку теперь это 32-я опция (каждая из которых занимает 4 бита для DEFERRED/OFF/ON/RESERVED), теперь, таким образом, необходимо 132 бита, и PS_MITIGATION_OPTIONS_MAP расширена до 3 64-битных элементов массива в поле карты.
Новая функция KeVerifyContextIpForUserCet будет вызываться всякий раз, когда собирается изменить контекст потока. Она проверит, что и CET, и защита указателя RIP включены для потока, а также проверит, установлен ли флаг CONTEXT_CONTROL в параметре контекста, означая, что указатель RIP будет изменен этим новым контекстом. Если все эти проверки пройдены, она вызывает внутреннюю функцию KiVerifyContextIpForUserCet. Цель этой функции - проверить, что целевое значение RIP является допустимым значением, а не значением, используемым эксплоитом для запуска произвольного кода.
Сначала она проверяет, что целевой адрес RIP не является адресом ядра, а также не адресом в младших байтах 0x10000, который не должен отображаться. Затем она извлекает этот базовый кадр-ловушку и проверяет, является ли целевой RIP, RIP этого кадра-ловушки. Это предназначено для разрешения случаев, когда целевой RIP является предыдущим адресом в режиме пользователя. Это обычно происходит, когда это первый раз, когда NtSetThreadContext вызывается для этого потока, а RIP устанавливается в качестве начального адреса для потока, но также может происходить и в других, менее распространенных случаях.
Функция получает KCONTINUE_TYPE и, основываясь на его значении, обрабатывает целевой RIP различными способами. В большинстве случаев она будет перебирать теневой стек и искать целевой RIP. Если она не находит его, он будет продолжать работать до тех пор, пока не сработает исключение и не получит обработчик исключений. Обработчик исключений проверит, является ли предоставленный KCONTINUE_TYPE KCONTINUE_UNWIND, и если это так вызывает функцию RtlVerifyUserUnwindTarget с флагом KCONTINUE_UNWIND. Эта функция попытается снова проверить RIP, на этот раз используя более сложные проверки, которые мы опишем в следующем разделе.
В любом другом случае, функция вернет STATUS_SET_CONTEXT_DENIED, что заставит KeVerifyContextIpForUserCet вызвать функцию KiLogUserCetSetContextIpValidationAudit, чтобы проверить сбой, если в AuditUserCetSetContextIpValidationC установлен флаг. Этот "аудит" довольно интересен, так как вместо того, чтобы проводиться по обычному каналу безопасности ETW процессов, он выполняется путем непосредственного вызова исключения быстрого сбоя с помощью службы отчетов об ошибках Windows (WER) (то есть отправки исключения 0xC000409 с информацией установить в FAST_FAIL_SET_CONTEXT_DENIED). Чтобы избежать спама WER, используется другой бит EPROCESS, AuditUserCetSetContextIpValidationLogged.
В одном случае функция прекратит итерацию по теневому стеку, прежде чем найдет целевой указатель RIP - если поток завершается и текущий адрес теневого стека теней выровнен по странице. Это означает, что для завершающих потоков функция будет пытаться проверить целевой RIP только на текущей странице стека теней как "best effort", но не пойдет дальше. Если он не найдет целевой RIP на этой странице, она вернет STATUS_THREAD_IS_TERMINATING.
Другой случай в этой функции - когда KCONTINUE_TYPE имеет значение KCONTINUE_LONGJUMP. Тогда целевой RIP не будет проверен по стеку, но вместо этого будет вызвана функция RtlVerifyUserUnwindTarget с флагом KCONTINUE_LONGJUMP для проверки RIP в таблице longjmp каталога конфигурации загрузки отображений PE. Мы опишем эту таблицу и эти проверки в следующем разделе этого блога.
KeVerifyContextIpForUserCet вызывается одной из следующих двух функций:
Все пути, которые вызывают KeVerifyContextIpForUserCet для проверки целевого RIP, сначала вызывают функцию KeVerifyContextXStateCetU для проверки целевого SSP и выполняют только проверки RIP, если определено, что SSP является действительным.
Раскрутка исключений и проверка Longjmp
Как показано выше, обработка для KCONTEXT_SET и KCONTEXT_RESUME связана с проверкой того, что целевой RIP является частью теневого стека, но другие сценарии (KCONTEXT_UNWIND и KCONTEXT_LONGJMP) требуют расширенной проверки с помощью функции RtlVerifyUserUnwind. Этот второй путь проверки содержит ряд интересных сложностей, которые требовали изменений в формате PE-файла (и поддержки компилятора), а также новый класс информации на уровне ОС, добавленный в NtSetInformationProcess для поддержки компилятора JIT.
Уже добавленный из-за улучшений поддержки Control Flow Guard (CFG), Каталог Конфигурации Загрузки Образа внутри PE-файла теперь содержит информацию для допустимых целей ветвления, используемых как часть пары setjmp/longjmp, которую должен идентифицировать современный компилятор и передать в компоновщик. В CET эти существующие данные используются повторно, но добавляется еще одна таблица и размер для поддержки продолжения обработчика исключений. В то время как Visual Studio 2017 производит таблицу longjmp, только Visual Studio 2019 создает эту более новую таблицу.
В этом последнем разделе мы рассмотрим формат этих таблиц и то, как ядро может авторизовать два последних типа потоков управления KCONTINUE_TYPE.
Таблицы метаданных PE
В дополнение к стандартной таблице GFIDS, присутствующей в образе Control Flow Guard, в Windows 10 также добавлена поддержка проверки целей longjmp путем включения Таблицы Длинных Целевых Переходов , обычно расположенной в разделе PE с именем .gljmp, RVA которого хранится в поле GuardLongJumpTargetTable в каталоге конфигурации загрузки изображений.
Всякий раз, когда в коде делается вызов setjmp, в эту таблицу добавляется RVA обратного адреса (в который будет переходить longjmp). Наличие этой таблицы определяется флагом IMAGE_GUARD_CF_LONGJUMP_TABLE_PRESENT в GuardFlags каталога конфигурации загрузки образа и содержит столько записей, сколько указано в поле GuardLongJumpTargetCount.
Каждая запись представляет собой 4-байтовый адрес RVA плюс n байтов метаданных, где n берется из результата (GuardFlags & IMAGE_GUARD_CF_FUNCTION_TABLE_SIZE_MASK) >> IMAGE_GUARD_CF_FUNCTION_TABLE_SIZE_SHIFT. Для этой таблицы метаданные не определены, поэтому ожидается, что байты метаданных всегда будут равны нулю. Интересно, что, поскольку этот расчет такой же, как и для таблицы GFIDS (которая может содержать метаданные, если включено подавление экспорта), подавление хотя бы одной цели CFG приведет к добавлению 1 байта пустых метаданных к каждой записи в Long Jump Target Table.
Например, вот PE-файл с двумя целями longjmp:
Обратите внимание на значение 1 в верхнем полубайте GuardFlags (что соответствует IMAGE_GUARD_CF_FUNCTION_TABLE_SIZE_MASK) из-за того, что этот образ также использует подавление экспорта CFG. Это говорит нам о том, что один дополнительный байт метаданных будет присутствовать в Long Jump Target Table, которую вы можете увидеть ниже:
В Windows 10 версии 20H1 метаданные этого типа теперь включены в одну дополнительную ситуацию - когда цели продолжения обработчика исключений присутствуют как часть потока управления двоичного файла. Два новых поля - GuardEHContinuationTable и GuardEHContinuationCount - добавляются в конец каталога конфигурации загрузки образа, и флаг IMAGE_GUARD_EH_CONTINUATION_TABLE_PRESENT теперь является частью GuardFlags. Структура этой таблицы идентична той, которая показана для Long Jump Target Table включая добавление байтов метаданных, основанных на верхнем полубайте GuardFlags.
К сожалению, даже текущие предварительные версии Visual Studio 2019 не генерируют эти данные, поэтому в настоящее время мы не можем показать вам пример - этот анализ основан на реверсинге кода проверки, который мы опишем позже, а также заголовочного файла Ntimage.h в 20H1 SDK.
Таблица обращенных пользователем функций
Теперь, когда мы знаем, что изменения потока управления могут произойти для перехода к цели longjmp или цели продолжения обработчика исключений, возникает вопрос - как мы можем получить эти две таблицы на основе RIP-адреса, присутствующего в структуре CONTEXT_EX, как части вызова функции NtContinueEx? Поскольку эти операции могут часто происходить в контексте выполнения некоторых программ, ядру необходим эффективный способ решения этой проблемы.
Возможно, вы уже знакомы с концепцией таблицы Инвертированных Функций. Такая таблица используется библиотекой Ntdll.dll (LdrpInvertedFunctionTable) для поиска кодов операций раскрутки и данных исключений во время обработки исключений в пользовательском режиме (например, путем поиска раздела .pdata). Другая таблица присутствует в файле Ntoskrnl.exe (PsInvertedFunctionTable) и используется во время обработки исключений режима ядра, а также в качестве части проверок PatchGuard.
Короче говоря, Таблица Инвертированных Функций - это массив, содержащий все загруженные модули пользователя/ядра по их размеру и указатель на каталог исключений PE, отсортированный по виртуальному адресу. Первоначально он был создан как оптимизация, поскольку поиск в этом массиве выполняется намного быстрее, чем синтаксический анализ заголовка PE, а затем поиск в связанном списке загруженных модулей - бинарный поиск в таблице инвертированных функций быстро найдет любой виртуальный адрес в соответствующем модуле только как log(n). Ken Johnson и Matt Miller, ныне известные Microsoft, ранее опубликовали подробный обзор в рамках своей статьи о методах перехвата в режиме ядра в журнале Uninformed.
Ранее, однако, библиотека Ntdll.dll только сканировал свою таблицу на предмет исключений пользовательского режима, а ядро Ntoskrnl.exe только сканировало свой аналога на предмет исключений режима ядра - изменения в версии 20H1 заключаются в том, что ядру теперь придется сканировать также пользовательскую таблицу - как часть новой логики, необходимой для обработки longjmp и исключений. Для поддержки этого добавлена новая функция RtlpLookupUserFunctionTableInverted, которая сканирует переменную KeUserInvertedFunctionTable, сопоставляя ее с теперь экспортированным символом LdrpInvertedFunctionTable в Ntdll.dll.
Это захватывающая криминалистическая возможность, так как она означает, что теперь у вас есть простой способ найти модули пользовательского режима, загруженные в текущем процессе, без необходимости разбирать данные загрузчика PEB или перечислять VAD. Например, вот как вы можете видеть текущие загруженные образы в процессе Csrss.exe:
При этом существует, хотя и удаленная, возможность того, что образ не содержит каталог исключений, особенно в системах x86, где не существует опкодов расскрутки, и секция .pdata создается только в том случае, если используется /SAFESEH и существует хотя бы один обработчик исключений.
В этих ситуациях RtlpLookupUserFunctionTableInverted может завершиться ошибкой, и вместо этого необходимо использовать функцию MmGetImageBase. Неудивительно, что при этом ищется любой VAD, который отображает регион, соответствующий входному указателю RIP, и, если это образ VAD, возвращает базовый адрес и размер региона (который должен соответствовать адресу модуля).
Цели обработчика динамических исключений
Последнее препятствие существует при обработке запросов KCONTINUE_UNWIND - хотя обычные процессы имеют в своих кодах цели продолжения обработчика статических исключений, основанные на предложениях __try/__except/__ finally, Windows позволяет механизмам JIT не только динамически создавать исполняемый код на лету, но и зарегистрировать обработчики исключений (и раскрутить опкоды) для него во время выполнения, например, через вызов RtlAddFunctionTable. Хотя эти обработчики исключений ранее были нужны только для обхода стека в пользовательском режиме и расскрутки исключений, теперь обработчики продолжения становятся законными целями потока управления, которые ядро должно понимать как потенциально допустимые значения для RIP. Это последняя возможность, которую обрабатывает RtlpFindDynamicEHContinuationTarget.
В рамках поддержки CET и введения NtContinueEx структура EPROCESS была расширена двумя новыми полями, названными DynamicEHContinuationTargetsLock и DynamicEHContinuationTargetsTree, первое из которых является EX_PUSH_LOCK, а второе - RTL_RB_TREE, которое содержит все действительные адреса обработчиков исключений. Это дерево управляется посредством вызова NtSetInformationProcess с новым классом информации о процессе, ProcessDynamicEHContinuationTargets, которая сопровождается структурой данных типа PROCESS_DYNAMIC_EH_CONTINUATION_TARGETS_INFORMATION, содержащий, в свою очередь массив записей PROCESS_DYNAMIC_EH_CONTINUATION_TARGET, которые будут проверены перед изменением DynamicEHContinuationTargetsTree. Чтобы упростить задачу, смотри определения ниже для этих структур и флагов:
Функция PspProcessDynamicEHContinuationTargets вызывается для итерации по этим данным, в какой момент RtlAddDynamicEHContinuationTarget вызываются для любого элемента, содержащего установленный флаг DYNAMIC_EH_CONTINUATION_TARGET_ADD, который выделяет структуру данных, хранящую целевой адрес, и связывая его RTL_BALANCED_NODE связь с RTL_RB_TREE в EPROCESS. И наоборот, если флаг отсутствует, то цель ищется, и если она действительно существует, удаляется и ее узел освобождается. По мере обработки каждой записи флаг DYNAMIC_EH_CONTINUATION_TARGET_PROCESSED вставляется в исходный буфер ввода OR, чтобы вызовы могли знать, какие записи работали, а какие — нет.
Очевидно, что существование этой возможности является универсальным обходом любой CET/CFG-подобной возможности, поскольку каждый возможный гаджет ROP может быть просто добавлен как "цель динамического продолжения". Однако, поскольку Microsoft в настоящее время только законно поддерживает JIT-компиляцию вне процесса для браузеров и Flash, важно отметить, что этот API работает только для удаленных процессов. Фактически, вызов этого в текущем процессе всегда будет неудачным со статусом STATUS_ACCESS_DENIED.
Целевая проверка
Объединяя все эти знания вместе, функцию RtlVerifyUserUnwindTarget становится довольно легко объяснить.
Заключение
Внедрение CET и связанные с ним меры по безопасности являются важным шагом на пути к устранению использования ROP и других методов перехвата потока управления. Целостность потока управления, очевидно, является сложной темой, которая, вероятно, станет еще более сложной, поскольку в будущем к ней будут добавлены дополнительные средства защиты. Дальнейшие проблемы совместимости и одноразовые сценарии, вероятно, приведут к тому, что будет обнаружено все больше и больше случаев, которые потребуют особой обработки. Тем не менее, такой большой шаг в технологии защиты, особенно такой, который включает в себя так много новых функций, неизбежно будет иметь пробелы и проблемы, и мы уверены, что по мере проведения дополнительных исследований в этой области, интересные вещи будут обнаружены там и в будущее.
источник https://windows-internals.com/cet-on-windows/
Перевод: yashechka, специально для https://xss.pro
Напомним, что Intel CET - это аппаратное средство защиты, которое устраняет два типа нарушений целостности потока управления, обычно используемых эксплоитами: нарушение forward-edge (косвенные инструкции CALL и JMP) и нарушение backward-edge (инструкции RET).
В то время как реализация forward-edge менее интересна (так как это по существу более слабая форма clang-cfi (https://clang.llvm.org/docs/ControlFlowIntegrity.html), похожая на Microsoft Control Flow Guard (https://docs.microsoft.com/en-us/windows/win32/secbp/control-flow-guard), реализация backward-edge опирается на фундаментальное изменение в ISA: введение нового стека называемого "Теневой Стек", который теперь реплицирует адреса возврата, которые помещаются в стек инструкцией CALL, с инструкцией RET, теперь проверяющей значения как стека, так и теневого стека, и генерирующей прерывание INT #21 (Ошибка Защиты Потока Управления) в случай несоответствия.
Поскольку операционные системы и компиляторы должны иногда поддерживать последовательности потоков управления, отличные от инструкций CALL/RET (такие как раскрутка исключений и механизм longjmp (https://en.cppreference.com/w/cpp/utility/program/longjmp)), иногда необходимо манипулировать "Указателем Теневого Стека" (SSP) на уровне системы, чтобы соответствовать требуемому поведению - и в свою очередь, проверять, чтобы сама эта манипуляция не стала потенциальным обходом. В этом посте мы расскажем, как Windows добивается этого.
Прежде чем углубляться в то, как Windows манипулирует и проверяет теневой стек для потоков, необходимо разобраться с двумя частями его реализации. Первое - это фактическое местонахождение и разрешения SSP, а второе - это механизм, используемый для хранения/восстановления SSP при переключении контекста между потоками, а также способы внесения изменений в SSP при необходимости (например, во время раскрутки исключения).
Чтобы объяснить эти механизмы, нам нужно углубиться в функцию центрального процессора Intel, которая была первоначально введена Intel для поддержки инструкций "Advanced Vector eXtensions" (AVX) (https://en.wikipedia.org/wiki/Advanced_Vector_Extensions) и впервые поддержана Microsoft в операционной системе Windows 7. А поскольку добавление поддержки для этой функции потребовало масштабной реструктуризации структуры CONTEXT в недокументированную структуру CONTEXT_EX (и добавление документированных и нативных функций для ее манипулирования), нам также придется поговорить об ее внутренностях!
Наконец, нам даже придется пройтись по некоторым внутренним компонентам форматов файлов компилятора и PE, а также новым классам информации о процессах, чтобы охватить дополнительные тонкости и требования к функциональности CET в Windows. Мы надеемся, что приведенное ниже оглавление поможет вам разобраться в этом подробном описании этих возможностей. Кроме того, при необходимости можно получить аннотированный исходный код для различных недавно представленных функций, щелкнув имена функций на основе нашего связанного репозитория GitHub (https://github.com/yardenshafir/cet-research).
Оглавление
- XState Internals
** XSAVE Area
** XState Configuration
** XState Policy
** CET XSAVE Area Format
- CONTEXT_EX Internals
** CONTEXT_EX Structure
** Initializing a CONTEXT_EX
** Controlling XState Feature Masks in CONTEXT_EX
** Locating XState Features in a CONTEXT_EX
** Example Usage and Output
- CONTEXT_EX Validation
** NtContinueEx and KCONTINUE_ARGUMENT
** Shadow Stack Pointer (SSP) Validation
** Instruction Pointer (RIP) Validation
- Exception unwinding and longjmp Validation
** PE Metadata Tables
** User Inverted Function Table
** Dynamic Exception Handler Continuation Targets
** Target Validation
Внутренности XState
Процессоры класса архитектуры x86-x64 изначально начинались с простого набора регистров, с которыми знакомы большинство исследователей безопасности - регистры общего назначения (RAX, RCX), регистры управления (например, RIP/RSP), регистры с плавающей запятой (XMM, YMM, ZMM) и некоторые регистры управления, отладки и тестирования. Однако по мере добавления дополнительных возможностей процессора необходимо было определить новые регистры, а также регистры конкретного состояние процессора, связанные с этими возможностями. А поскольку многие из этих функций являются локальными для потока, они должны быть сохранены и восстановлены во время переключения контекста.
В ответ, Intel определила спецификацию (https://www.intel.com/content/dam/w...-architectures-software-developers-manual.pdf) "eXtended State" (XState), которая связывает различные состояния процессора с битами в "Маске Состояний" и вводит дополнительные инструкции, такие как XSAVE и XRSTOR, для чтения и записи запрошенных состояний из "Области XSAVE". Поскольку эта область в настоящее время является критически важной частью хранилища регистров CET для каждого потока, и большинство людей в значительной степени игнорируют поддержку XSAVE из-за ее изначального внимания к функциям с плавающей запятой, AVX и "Расширения Защиты Памяти - Memory Protection eXtensions" (MPX) (https://software.intel.com/en-us/articles/introduction-to-intel-memory-protection-extensions), мы подумали, что обзор функциональности и расположение памяти будет полезно для читателей.
Область XSAVE
Как уже упоминалось, область XSAVE первоначально использовалась для хранения некоторых новых функциональных возможностей с плавающей запятой, таких как AVX, которые были добавлены в процессоры Intel, и для консолидации существующих состояний регистров x87 FPU и SSE, которые ранее сохранялись с помощью инструкций FXSTOR и FXRSTR. Эти первые два унаследованных состояния были определены как часть "Устаревшей области XSAVE", а любые дополнительные регистры процессора (такие как AVX) были добавлены в "Расширенную область XSAVE". Между ними "Заголовок области XSAVE" используется для описания того, какие расширенные функции присутствуют через маску состояния, называемую XSTATE_BV.
В то же время, был добавлен новый "Расширенный Регистр Управления - eXtended Control Register" (XCR0), который определяет, какие состояния поддерживаются операционной системой как часть функциональности XSAVE, а инструкции XGETBV и XSETBV были добавлены для настройки регистра XCR0 (и, возможно, в будущем XCR также). Например, операционные системы могут выбрать программирование регистра XCR0 так, чтобы он не содержало биты состояния функции для регистров x87 FPU и SSE, что означает, что они будут сохранять эту информацию вручную с устаревшими инструкциями FXSTOR и сохранят только расширенное состояние функции в своих областях XSAVE.
По мере роста количества расширенных наборов и возможностей регистров, таких как "Ключи Защиты Памяти - Memory Protection Keys-https://www.kernel.org/doc/html/latest/core-api/protection-keys.html" (MPK), в которые добавлено "Состояние Пользовательского Регистра Ключей Защиты - Protection Key Register User State" (PKRU), новые процессоры вводили различие между "Состоянием Супервизора", которое может только быть измененным кодом CPL0 с использованием инструкций XSAVES и XRSRTORS, а также версий "сжатия" и "оптимизации" (XSAVEC/XSAVEOPT), чтобы усложнить ситуацию в типичном для Intel стиле. Новый "Специфичный Регистр Модели - Model Specific Register" (MSR), названный IA32_XSS, был добавлен, чтобы определить, какие состояния доступны только для супервизора.
Механизм "оптимизированного XSAVE" существует, чтобы гарантировать, что только состояние процессора, которое фактически было изменено другим потоком с момента последнего переключения контекста (если есть), будет фактически записано в области XSAVE. Для отслеживания этой информации существует внутренний регистр процессора XINUSE. Когда используется XSAVEOPT, маска XSTATE_BV теперь включает только биты, соответствующие состояниям, которые были фактически сохранены, а не просто биты всех запрошенных состояний.
Механизм "сжатого XSAVE", с другой стороны, исправил расточительный недостаток в дизайне XState: по мере добавления все более и более расширенных функций, таких как AVX512 и "Intel Processor Trace (https://software.intel.com/en-us/blogs/2013/09/18/processor-tracing)" (IPT), это означало, что даже для потоков, которые не использовали эти возможности, достаточно большая область XSAVE должна была быть выделена и записана (заполнена нулями) процессором. Хотя оптимизированный XSAVE позволит избежать этих записей, это все равно означает, что любые расширенные функции, следующие за большими, но еще неиспользованными состояниями, будут находиться на больших смещениях от основного буфера области XSAVE.
В XSAVEC эта проблема решается путем использования только пространства для сохранения функций XState, которые фактически включены (и используются, поскольку сжатие подразумевает оптимизацию) текущим потоком, и последовательно размещают каждое сохраненное состояние в памяти без пропусков между ними. (но потенциально с фиксированным 64-байтовым выравниванием, которое предоставляется как часть "маски выравнивания" через инструкцию CPUID). Заголовок области XSAVE, показанный ранее, теперь расширен второй маской состояния, называемой XCOMP_BV, которая указывает, какие из запрошенных битов состояния, которые были запрошены, могут присутствовать в вычисляемой области. Обратите внимание, что в отличие от XSTATE_BV, эта маска не пропускает биты состояния, которые не были частью XINUSE - она включает в себя все возможные биты, которые могли быть сжаты - все равно необходимо проверить XSTATE_BV, чтобы определить, какие области состояния действительно присутствуют. Наконец, бит 63 всегда устанавливается в XCOMP_BV, когда использовалась сжатая инструкция, как индикатор того, для какого формата имеет область XSAVE.
Таким образом, использование сжатого и не сжатого формата определяет внутреннюю разметку и размер области XSAVE. Сжатый формат будет выделять память в области XSAVE только для функций процессора, используемых потоком, в то время как несжатый будет выделять память для всех функций процессора, поддерживаемых процессором, но заполнять только те, которые используются потоком. На диаграмме ниже показан пример того, как будет выглядеть область XSAVE для того же потока, но при использовании одного и другого формата.
Подводя итог, в котором говорится, что семейство инструкций XSAVE * / XRSTOR * будет работать с комбинацией:
- Какие биты состояния операционной системы утверждает, что она поддерживает в XCR0 (устанавливается с помощью инструкции XSETBV)
- Какие биты состояния хранит вызывающая сторона в паре регистров EDX:EAX при использовании инструкции XSAVE (Intel называет это "маской инструкции")
- При использовании непривилегированных инструкций, какие биты состояния не установлены в IA32_XSS
- На процессорах, которые поддерживают "Оптимизированный XSAVE", биты состояния которого установлены в XINUSE, внутреннем регистре, который отслеживает фактические регистры, связанные с XState, которые использовались текущим потоком с момента последнего перехода
Как только эти биты маскируются вместе, окончательный набор результирующих битов состояния записывается командой XSAVE в заголовок области XSAVE в поле, называемом XSTATE_BV. В случае, когда используется "сжатый XSAVE", результирующие биты состояния, пропускающие пункт 4 (XINUSE), записываются в заголовок области XSAVE в поле XCOMP_BV. На схеме ниже показаны полученные маски.
Конфигурация Xstate
Поскольку каждый процессор имеет свой собственный набор функций, потенциальных размеров, возможностей и механизмов с поддержкой XState, Intel предоставляет всю эту информацию через различные классы инструкции CPUID, которые операционная система должна запрашивать при работе с XState. Windows выполняет эти запросы при загрузке и сохраняет информацию в структуре XSTATE_CONFIGURATION, которая показана ниже (документировано в файле Winnt.h)
C:
typedef struct _XSTATE_CONFIGURATION
{
ULONG64 EnabledFeatures;
ULONG64 EnabledVolatileFeatures;
ULONG Size;
union
{
ULONG ControlFlags;
struct
{
ULONG OptimizedSave:1;
ULONG CompactionEnabled:1;
};
};
XSTATE_FEATURE Features[MAXIMUM_XSTATE_FEATURES];
ULONG64 EnabledSupervisorFeatures;
ULONG64 AlignedFeatures;
ULONG AllFeatureSize;
ULONG AllFeatures[MAXIMUM_XSTATE_FEATURES];
ULONG64 EnabledUserVisibleSupervisorFeatures;
} XSTATE_CONFIGURATION, *PXSTATE_CONFIGURATION;
После заполнения этих данных, ядро сохраняет эту информацию в структуре KUSER_SHARED_DATA, к которой можно получить доступ через переменную SharedUserData и расположенную по адресу 0x7FFE0000 на всех платформах Windows.
Например, вот выходные данные нашей тестовой системы версии 19H1, которая поддерживает как оптимизированные, так и сжатые формы области XSAVE и имеет функциональные биты x87 FPU(0), SSE(1), AVX(2) и MPX(3, 4) включенными.
Bash:
dx ((nt!_KUSER_SHARED_DATA*)0x7ffe0000)->XState
[+0x000] EnabledFeatures : 0x1f [Type: unsigned __int64]
[+0x008] EnabledVolatileFeatures : 0xf [Type: unsigned __int64]
[+0x010] Size : 0x3c0 [Type: unsigned long]
[+0x014] ControlFlags : 0x3 [Type: unsigned long]
[+0x014 ( 0: 0)] OptimizedSave : 0x1 [Type: unsigned long]
[+0x014 ( 1: 1)] CompactionEnabled : 0x1 [Type: unsigned long]
[+0x018] Features [Type: _XSTATE_FEATURE [64]]
[+0x218] EnabledSupervisorFeatures : 0x0 [Type: unsigned __int64]
[+0x220] AlignedFeatures : 0x0 [Type: unsigned __int64]
[+0x228] AllFeatureSize : 0x3c0 [Type: unsigned long]
[+0x22c] AllFeatures [Type: unsigned long [64]]
[+0x330] EnabledUserVisibleSupervisorFeatures : 0x0 [Type: unsigned __int64]
В массиве Features можно найти размер и смещение каждой из этих пяти функций:
Bash:
dx -r2 (((nt!_KUSER_SHARED_DATA*)0x7ffe0000)->XState)->Features.Take(5)
[0] [Type: _XSTATE_FEATURE]
[+0x000] Offset : 0x0 [Type: unsigned long]
[+0x004] Size : 0xa0 [Type: unsigned long]
[1] [Type: _XSTATE_FEATURE]
[+0x000] Offset : 0xa0 [Type: unsigned long]
[+0x004] Size : 0x100 [Type: unsigned long]
[2] [Type: _XSTATE_FEATURE]
[+0x000] Offset : 0x240 [Type: unsigned long]
[+0x004] Size : 0x100 [Type: unsigned long]
[3] [Type: _XSTATE_FEATURE]
[+0x000] Offset : 0x340 [Type: unsigned long]
[+0x004] Size : 0x40 [Type: unsigned long]
[4] [Type: _XSTATE_FEATURE]
[+0x000] Offset : 0x380 [Type: unsigned long]
[+0x004] Size : 0x40 [Type: unsigned long]
Политика XState
К сожалению, несмотря на то, что процессор может претендовать на поддержку определенной функции XState, часто оказывается, что из-за различных аппаратных ошибок некоторые конкретные процессоры могут не полностью или неправильно поддерживать эту функцию в конце концов. Для обработки этой возможности Windows использует политику XState, которая представляет собой информацию, хранящуюся в разделе ресурсов Драйвера Политики Оборудования, который обычно называется HwPolicy.sys.
Поскольку архитектура Intel x86 представляет собой комбинацию множества поставщиков процессоров, конкурирующих с вариантами наборов функций друг друга, ядро должно проанализировать политику XState и сравнить текущую строку поставщика и Версию Микрокода, а также его Подпись, Функции и Расширенные функции. (а именно, регистры RAX, RDX и RCX из запроса инструкции CPUID 01h)
Эта работа выполняется при загрузке функцией KiIntersectFeaturesWithPolicy, которая вызывается функцию KiInitializeXSave, которая вызывает KiLoadPolicyFromImage для загрузки соответствующей политики XState, вызывает KiGetProcessorInformation для получения данных центрального процессора, упомянутых ранее, а затем проверяет каждый бит функции, включенный в настоящее время в конфигурации XState, посредством обращений к KiIsXSaveFeatureAllowed.
Эти функции работают с ресурсом 101 в драйвере HwPolicy.sys, который начинается со следующей структуры данных:
C:
typedef struct _XSAVE_POLICY
{
ULONG Version;
ULONG Size;
ULONG Flags;
ULONG MaxSaveAreaLength;
ULONGLONG FeatureBitmask;
ULONG NumberOfFeatures;
XSAVE_FEATURE Features[1];
} XSAVE_POLICY, *PXSAVE_POLICY;
Например, в нашей системе версии 19H1 содержимое (которое мы извлекли с помощью Resource Hacker) было следующим:
Bash:
dx @$policy = (_XSAVE_POLICY*)0x253d0e90000
[+0x000] Version : 0x3 [Type: unsigned long]
[+0x004] Size : 0x2fd8 [Type: unsigned long]
[+0x008] Flags : 0x9 [Type: unsigned long]
[+0x00c] MaxSaveAreaLength : 0x2000 [Type: unsigned long]
[+0x010] FeatureBitmask : 0x7fffffffffffffff [Type: unsigned __int64]
[+0x018] NumberOfFeatures : 0x3f [Type: unsigned long]
[+0x020] Features [Type: _XSAVE_FEATURE [1]]
Для каждого XSAVE_FEATURE находится смещение в структуре XSAVE_VENDORS, которое содержит массив структур XSAVE_VENDOR, каждая из которых имеет строку поставщика центрального процессора (на данный момент каждая из них выглядит как "GenuineIntel", "AuthenticAMD" или "CentaurHauls"), и смещение к структуре XSAVE_CPU_ERRATA. Например, наша тестовая система версии 19H1 имела следующую информацию для функции 0:
C:
dx -r4 @$vendor = (XSAVE_VENDORS*)((int)@$policy->Features[0].Vendors + 0x253d0e90000)
[+0x000] NumberOfVendors : 0x3 [Type: unsigned long]
[+0x008] Vendor [Type: _XSAVE_VENDOR [1]]
[0] [Type: _XSAVE_VENDOR]
[+0x000] VendorId [Type: unsigned long [3]]
[0] : 0x756e6547 [Type: unsigned long]
[1] : 0x49656e69 [Type: unsigned long]
[2] : 0x6c65746e [Type: unsigned long]
[+0x010] SupportedCpu [Type: _XSAVE_SUPPORTED_CPU]
[+0x000] CpuInfo [Type: XSAVE_CPU_INFO]
[+0x020] CpuErrata : 0x4c0 [Type: XSAVE_CPU_ERRATA *]
[+0x020] Unused : 0x4c0 [Type: unsigned __int64]
Наконец, каждая структура XSAVE_CPU_ERRATA содержит соответствующие информационные данные процессора, которые соответствуют известным ошибкам, которые препятствуют поддержке указанной функции XState. Например, в нашей тестовой системе первые ошибки из смещения выше были:
Bash:
dx -r3 @$errata = (XSAVE_CPU_ERRATA*)((int)@$vendor->Vendor[0].SupportedCpu.CpuErrata + 0x253d0e90000)
[+0x000] NumberOfErrata : 0x1 [Type: unsigned long]
[+0x008] Errata [Type: XSAVE_CPU_INFO [1]]
[0] [Type: XSAVE_CPU_INFO]
[+0x000] Processor : 0x0 [Type: unsigned char]
[+0x002] Family : 0x6 [Type: unsigned short]
[+0x004] Model : 0xf [Type: unsigned short]
[+0x006] Stepping : 0xb [Type: unsigned short]
[+0x008] ExtendedModel : 0x0 [Type: unsigned short]
[+0x00c] ExtendedFamily : 0x0 [Type: unsigned long]
[+0x010] MicrocodeVersion : 0x0 [Type: unsigned __int64]
[+0x018] Reserved : 0x0 [Type: unsigned long]
Инструмент, который дампит политику аппаратного обеспечения вашей системы для всех функций XState, доступен на нашем GitHub (https://github.com/yardenshafir/cet-research/tree/master/Xpolicy) здесь. На данный момент во всей политике отображается только одна ошибка (показанная выше).
Наконец, следующие дополнительные параметры командной строки загрузчика (и соответствующие параметры BCD) могут использоваться для дальнейшей настройки возможностей Xstate:
- Параметр загрузки XSAVEPOLICY = n, установленный с помощью параметра BCD xsavepolicy, который устанавливает KeXSavePolicyId, указывая, какую загрузить из политик XState.
- Параметр загрузки XSAVEREMOVEFEATURE = n, установленный с помощью параметра BCD xsaveremovefeature, который устанавливает KeTestRemovedFeatureMask. Это будет позже проанализировано функцией KiInitializeXSave и исключит указанные биты состояния из поддержки. Обратите внимание, что State 0 (x87 FPU) и State 1 (SSE) не могут быть удалены таким образом.
- Параметр загрузки XSAVEDISABLE, установленный с помощью параметра BCD xsavedisable, который устанавливает KeTestDisableXsave и заставляет функцию KiInitializeXSave установить для всех данных конфигурации, связанных с XState, значение 0, полностью отключая функцию Xstate.
CET XSAVE Формат области
В рамках реализации CET корпорация Intel определила два новых бита в стандарте XState, которые называются XSTATE_CET_U (11) и XSTATE_CET_S (12), что соответствует состоянию пользователя и супервизора соответственно. Первое состояние представляет собой 16-байтовую структуру данных, которая MSDN документирует как XSAVE_CET_U_FORMAT, содержащая MSR IA32_U_CET (где настроен флаг "Включение Теневого Стека") и MSR IA32_PL3_SSP (где хранится "SSP уровня привилегий 3"). Второй, который еще не имеет определения MSDN, включает MSR IA32_PL0/1/2_SSP.
C:
typedef struct _XSAVE_CET_U_FORMAT
{
ULONG64 Ia32CetUMsr;
ULONG64 Ia32Pl3SspMsr;
} XSAVE_CET_U_FORMAT, *PXSAVE_CET_U_FORMAT;
typedef struct _XSAVE_CET_S_FORMAT
{
ULONG64 Ia32Pl0SspMsr;
ULONG64 Ia32Pl1SspMsr;
ULONG64 Ia32Pl2SspMsr;
} XSAVE_CET_S_FORMAT, *PXSAVE_CET_S_FORMAT;
Как следует из имен полей, "регистры", связанные с CET, на самом деле являются значениями, хранящимися в соответствующих MSR, к которым обычно можно получить доступ только через привилегированные инструкции RDMSR и WRMSR в кольце защиты 0. Однако, в отличие от большинства MSR, в которых хранятся глобальные данные процессора, CET можно включить для каждого потока, а указатель теневого стека также явно для каждого потока. По этим причинам данные, связанные с CET, должны стать частью функциональности XState, чтобы операционные системы могли правильно обрабатывать переключатели потоков.
Поскольку регистры CET в основном являются регистрами MSR, которые обычно могут быть изменены только кодом ядра, они не доступны через инструкции CPL3 XSAVE/XRSTOR, и их соответствующие биты состояния всегда устанавливаются в 1 в MSR IA32_XSS. Однако, что усложняет ситуацию, так это то, что операционная система не может полностью заблокировать код пользовательского режима от изменения SSP. Код пользовательского режима может на законных основаниях нуждаться в обновлении SSP как части обработки исключений, раскрутки, setjmp/longjmp или определенных функций, таких как механизм "Нитей" в Windows.
Таким образом, операционные системы должны предоставлять возможность потокам изменять состояние CET в XState посредством системного вызова, подобно тому, как Windows предоставляет функция SetThreadContext как механизм обновления определенных защищенных регистров центрального процессора , таких как CS и DR7, при условии соблюдения определенных правил. Поэтому, в следующем разделе мы увидим, как структура CONTEXT превратилась в структуру CONTEXT_EX в более современных версиях Windows для поддержки информации, связанной с XState, и как должна была быть добавлена специфичная для CET обработка для законных сценариев, связанных с исключениями, в то же время избегая злонамеренных атак потока управления через поврежденные контексты.
Внутреннее устройство структуры CONTEXT_EX
Для поддержки растущего числа регистров, которые должны быть сохранены при каждом переключении контекста, новые версии Windows имеют структуру CONTEXT_EX в дополнение к устаревшей структуре CONTEXT. Это было необходимо из-за того, что CONTEXT является структурой фиксированного размера, в то время как XSAVE ввел потребность в данных о состоянии процессора динамического размера, которые зависят от потока, процессора и даже политики конфигурации компьютера.
Структура CONTEXT_EX
К сожалению, хотя в настоящее время используется во всех функциях обработки исключений ядра и пользовательского режима, структура CONTEXT_EX в значительной степени недокументирована, за исключением случайного выпуска некоторой информации в заголовочных файлах Windows 7 и некоторого справочного кода Intel (что может свидетельствовать о том, что на самом деле Intel ответственна за определение этой мерзости). Просто посмотрите на этот блок комментариев и скажите нам, можете ли вы что-нибудь понять:
C:
//
// This structure specifies an offset (from the beginning of CONTEXT_EX
// structure) and size of a single chunk of an extended context structure.
//
// N.B. Offset may be negative.
//
typedef struct _CONTEXT_CHUNK
{
LONG Offset;
DWORD Length;
} CONTEXT_CHUNK, *PCONTEXT_CHUNK;
//
// CONTEXT_EX structure is an extension to CONTEXT structure. It defines
// a context record as a set of disjoint variable-sized buffers (chunks)
// each containing a portion of processor state. Currently there are only
// two buffers (chunks) are defined:
//
// - Legacy, that stores traditional CONTEXT structure;
// - XState, that stores XSAVE save area buffer starting from
// XSAVE_AREA_HEADER, i.e. without the first 512 bytes.
//
// There a few assumptions exists that simplify conversion of PCONTEXT
// pointer to PCONTEXT_EX pointer.
//
// 1. APIs that work with PCONTEXT pointers assume that CONTEXT_EX is
// stored right after the CONTEXT structure. It is also assumed that
// CONTEXT_EX is present if and only if corresponding CONTEXT_XXX
// flags are set in CONTEXT.ContextFlags.
//
// 2. CONTEXT_EX.Legacy is always present if CONTEXT_EX structure is
// present. All other chunks are optional.
//
// 3. CONTEXT.ContextFlags unambigiously define which chunks are
// present. I.e. if CONTEXT_XSTATE is set CONTEXT_EX.XState is valid.
//
typedef struct _CONTEXT_EX
{
//
// The total length of the structure starting from the chunk with
// the smallest offset. N.B. that the offset may be negative.
//
CONTEXT_CHUNK All;
//
// Wrapper for the traditional CONTEXT structure. N.B. the size of
// the chunk may be less than sizeof(CONTEXT) is some cases (when
// CONTEXT_EXTENDED_REGISTERS is not set on x86 for instance).
// CONTEXT_CHUNK Legacy;
//
// CONTEXT_XSTATE: Extended processor state chunk. The state is
// stored in the same format XSAVE operation strores it with
// exception of the first 512 bytes, i.e. staring from
// XSAVE_AREA_HEADER. The lower two bits corresponding FP and
// SSE state must be zero.
// CONTEXT_CHUNK XState;
} CONTEXT_EX, *PCONTEXT_EX;
#define CONTEXT_EX_LENGTH ALIGN_UP_BY(sizeof(CONTEXT_EX), STACK_ALIGN)
//
// These macros make context chunks manupulations easier.
//
Таким образом, хотя эти заголовки пытаются объяснить структуру CONTEXT_EX, текст достаточно тупой (и полон ошибок на английском языке), так что нам потребовалось несколько раундов, пока мы не смогли его визуализировать, и почувствовал, что диаграмма может быть полезна ,
Как показано на схеме, структура CONTEXT_EX всегда находится в конце структуры CONTEXT и имеет 3 поля типа CONTEXT_CHUNK с именами All, Legacy и XState. Каждый из них определяет смещение и длину данных, связанных с ними, и существуют различные макросы RTL_ для извлечения соответствующего указателя данных.
Поле Legacy относится к началу исходной структуры CONTEXT (хотя длина может быть меньше на процессорах x86, если CONTEXT_EXTENDED_REGISTERS не указан). Поле "All" также относится к началу исходной структуры CONTEXT, но его длина описывает совокупность всех данных, включая сам CONTEXT_EX и пространство заполнения/выравнивания, необходимое для области XSAVE. Наконец, поле XState относится к структуре XSAVE_AREA_HEADER (которая затем определяет маску состояния, какие биты состояния включены и, следовательно, чьи данные присутствуют) и длину всей области XSAVE. Из-за этого макета важно отметить, что "All" и "Legacy" будут иметь отрицательные смещения.
Поскольку вся эта математика сложна, библиотека Ntdll.dll экспортирует различные API-интерфейсы, чтобы упростить построение, чтение, копирование и другие манипуляции с различными данными, хранящимися в структуре CONTEXT_EX (некоторые, но не все, эти API-интерфейсы внутренне используются ядром Ntoskrnl, но ничто не экспортируется). В свою очередь, библиотека KernelBase.dll экспортирует документированные функции Win32, которые внутренне используют эти возможности.
Инициализация структуры CONTEXT_EX
Во-первых, вызовы должны выяснить, какой объем памяти выделить для хранения структуры CONTEXT_EX, что можно сделать с помощью следующего API:
C:
NTSYSAPI
ULONG
NTAPI
RtlGetExtendedContextLength ( _In_ ULONG ContextFlags, _Out_ PULONG ContextLength);
Ожидается, что вызовы предоставят соответствующие флаги CONTEXT_XXX, чтобы указать, какие регистры они намереваются сохранить (а именно CONTEXT_XSTATE, в противном случае использование CONTEXT_EX на самом деле не приносит ничего). Эта функция затем читает SharedUserData.XState.EnabledFeatures и SharedUserData.XState.EnabledUserVisibleSupervisorFeatures и передает объединение всех битов в расширенную функцию (также экспортированную), показанную ниже.
C:
NTSYSAPI
ULONG
NTAPI
RtlGetExtendedContextLength2 (
_In_ ULONG ContextFlags,
_Out_ PULONG ContextLength,
_In_ ULONG64 XStateCompactionMask
);
Обратите внимание, что этот более новый API позволяет вручную указывать, какие состояния XState должны фактически сохраняться, вместо того, чтобы извлекать все включенные функции из конфигурации XState в общих пользовательских данных. Это приводит к тому, что структура CONTEXT_EX будет меньше и не будет содержать достаточно места для всех возможных Данных Состояния XState, поэтому при использовании этой структуры CONTEXT_EX в будущем не следует использовать Биты Состояния XState вне указанной маски.
Затем вызовы выделяет память для структуры CONTEXT_EX (в большинстве случаев Windows будет использовать функцию alloca(), чтобы избежать сбоев исчерпания памяти в путях исключений), и использует один из этих двух функций:
C:
NTSYSAPI
ULONG
NTAPI
RtlInitializeExtendedContext (
_Out_ PVOID Context,
_In_ ULONG ContextFlags,
_Out_ PCONTEXT_EX* ContextEx
);
NTSYSAPI
ULONG
NTAPI
RtlInitializeExtendedContext2 (
_Out_ PVOID Context,
_In_ ULONG ContextFlags,
_Out_ PCONTEXT_EX* ContextEx,
_In_ ULONG64 XStateCompactionMask
);
Как и прежде, новый API позволяет вручную указывать, какие состояния XState следует сохранять в их сжатой форме, в противном случае предполагается, что все доступные функции (основанные на SharedUserData) присутствуют. Очевидно, ожидается, что вызовы указывает те же флаги ContextFlags, что и при вызове RtlGetExtendedContextLength(2), чтобы убедиться, что структура контекста имеет правильный размер, как было выделено. В ответ вызовы теперь получает указатель на структуру CONTEXT_EX, которая, как ожидается, будет следовать за входным буфером CONTEXT.
Когда существует структуры CONTEXT_EX, вызов, скорее всего, сначала будет заинтересован в получении от него унаследованной структуры CONTEXT (без предположений о размерах), что можно сделать с помощью следующей функции:
C:
NTSYSAPI
PCONTEXT
NTAPI
RtlLocateLegacyContext (
_In_ PCONTEXT_EX ContextEx,
_Out_opt_ PULONG Length,
);
Однако, как уже упоминалось выше, это недокументированные и внутренние API-интерфейсы, предоставляемые уровнем NT в Windows. Легитимные приложения Win32 вместо этого упростят использование XState-совместимых структур CONTEXT, используя вместо этого следующие функции:
C:
WINBASEAPI
BOOL
WINAPI
InitializeContext (
_Out_writes_bytes_opt_(*ContextLength) PVOID Context,
_In_ DWORD ContextFlags,
_Out_ PCONTEXT_EX Context,
_Inout_ PDWORD ContextFlags
);
WINBASEAPI
BOOL
WINAPI
InitializeContext2 (
_Out_writes_bytes_opt_(*ContextLength) PVOID Context,
_In_ DWORD ContextFlags,
_Out_ PCONTEXT_EX Context,
_Inout_ PDWORD ContextFlags,
_In_ ULONG64 XStateCompactionMask
);
Эти две функции ведут себя подобно комбинации использования недокументированных функций: когда вызовы сначала передают NULL в качестве параметров Buffer и Context, функция возвращает требуемую длину в ContextLength, которую вызывающие абоненты должны выделить из памяти. При второй попытке, вызовы передают выделенный указатель в Buffer и получают указатель на структуру CONTEXT в Context без какого-либо знания базовой структуры CONTEXT_EX.
Управление масками объектов XState в структутре CONTEXT_EX
Чтобы получить доступ к XSTATE_BV (маска расширенной функции), которая глубоко встроена в поле XSAVE_AREA_HEADER в структуре CONTEXT_EX, система экспортирует два API-интерфейса для простой проверки, какие функции XState включены в структуру CONTEXT_EX, с соответствующим функциями для изменение маски Xstate.
Однако обратите внимание, что Windows никогда не хранит состояния x87 FPU(0) и SSE(1) в области XSAVE и вместо этого использует инструкцию FXSAVE, что означает, что область XSAVE никогда не будет содержать Устаревшую Область, и немедленно начнется с XSAVE_AREA_HEADER. Благодаря этому Get API всегда будет маскировать 2 нижних бита. Кроме того, API-интерфейс Set также гарантирует, что указанная функция присутствует в EnabledFeatures Конфигурации Xstate.
Имейте в виду, что если в InitializeContext2 (или внутренних нативных API-интерфейсах) была задана жестко закодированная маска сжатия, то API-интерфейс Set не следует использовать, кроме как для исключения существующих битов состояния (поскольку добавление нового бита подразумевает дополнительный неинициализированный выход данные о состоянии границ в CONTEXT_EX, который уже был бы предварительно выделен без этих данных).
C:
NTSYSAPI
ULONG64
NTAPI
RtlGetExtendedFeaturesMask (
_In_ PCONTEXT_EX ContextEx
);
NTSYSAPI
ULONG64
NTAPI
RtlSetExtendedFeaturesMask (
_In_ PCONTEXT_EX ContextEx,
_In_ ULONG64 FeatureMask
);
Документированная форма этих функций выглядит следующим образом:
C:
WINBASEAPI
BOOL
WINAPI
GetXStateFeaturesMask (
_In_ PCONTEXT Context
_Out_ PDWORD64 FeatureMask
);
NTSYSAPI
ULONG64
NTAPI
SetXStateFeaturesMask (
_In_ PCONTEXT Context,
_In_ DWORD64 FeatureMask
);
Нахождение объектов XState в структуре CONTEXT_EX
Из-за сложности структуры CONTEXT_EX, а также того факта, что функции XState могут присутствовать либо в сжатом, либо в не в сжатом виде, и что их присутствие также зависит от различных масок состояний, описанных ранее (особенно, если поддерживается оптимизированный XSAVE ), вызовам нужна библиотечная функция, чтобы быстро и легко получить указатель на соответствующие данные о состоянии в области XSAVE в пределах структуры CONTEXT_EX.
В настоящее время существуют две такие функции, как показано ниже, причем функция RtlLocateExtendedFeature является просто оболочкой для функции RtlLocateExtendedFeature2, которая снабжает ее указателем на SharedUserData.XState в качестве параметра конфигурации. Поскольку оба экспортируются, вызовы могут также вручную указать свою собственную пользовательскую конфигурацию XState в последней функции, если они того пожелают.
C:
NTSYSAPI
PVOID
NTAPI
RtlLocateExtendedFeature (
_In_ CONTEXT_EX ContextEx,
_In_ ULONG FeatureId,
_Out_opt_ PULONG Length
);
NTSYSAPI
PVOID
NTAPI
RtlLocateExtendedFeature2 (
_In_ CONTEXT_EX ContextEx,
_In_ ULONG FeatureId,
_In_ PXSTATE_CONFIGURATION Configuration,
_Out_opt_ PULONG Length
);
Обе эти две функции получают структуру CONTEXT_EX и идентификатор запрашиваемой функции и анализируют данные конфигурации XState, чтобы получить указатель на место хранения функции в области XSAVE. Обратите внимание, что они не проверяют и не возвращают какое-либо фактическое значение для указанной функции, которая зависит от вызывающей стороны.
Чтобы найти указатель, функция RtlLocateExtendedFeature2 делает следующее:
- Убеждается, чтобы идентификатор функции был выше 2 (поскольку состояния x87 FPU и SSE никогда не сохраняются через XSAVE в Windows) и ниже 64 (максимально возможный бит функции Xstate)
- Получает XSAVE_AREA_HEADER из CONTEXT_EX + CONTEXT_EX.XState.Offset
- Читает флаг Configuration-> ControlFlags.CompactionEnabled, чтобы узнать, используется ли сжатие или нет
- Если используется несжатый формат: читает Configuration->Features[n].Offset и .Size, чтобы узнать смещение и размер запрашиваемой функции в области XSAVE.
- Если используется сжатый формат:
- Считывает CompactionMask из XSAVE_AREA_HEADER (соответствует XCOMP_BV) и проверяет, содержит ли он запрошенную функцию
- Считывает Configuration->AllFeatures, чтобы узнать размеры всех включенных состояний, бит состояния которых предшествует запрошенному идентификатору функции, и вычисляет смещение запрошенного формата на основе суммирования этих размеров, выравнивая начало каждой предыдущей области состояния по 64. байт, если соответствующий бит установлен в Configuration->AlignedFeatures, и, наконец, при необходимости выравнивание начала области для указанного идентификатора объекта, если это необходимо
- Считывает размер запрошенной функции из Configuration.AllFeatures[n]
- Находит функцию в области XSAVE на основе его вычисленного смещения сверху и возвращает указатель на него, необязательно вместе с его соответствующим размером в выходной переменной Length.
Это означает, что для нахождения адреса определенной функции в несжатом формате достаточно проверить в SharedUserData, какие функции поддерживаются процессором. Однако в сжатом формате невозможно полагаться на смещения в SharedUserData, поэтому необходимо также проверить, какие функции включены в потоке, и рассчитать правильное смещение для объекта на основе размеров всех предыдущих функций.
В легитимных приложениях Win32 используются друие функции, который внутренне вызывает нативные функции выше, но с некоторой предварительной обработкой. Поскольку биты состояния 0 и 1 никогда не сохраняются как часть области XSAVE в структуре CONTEXT_EX, API-интерфейс Win32 обрабатывает эти два функциональных бита, извлекая их из соответствующих устаревших полей структуры CONTEXT, а именно FltSave для XSTATE_LEGACY_FLOATING_POINT и Xmm0 для XSTATE_LEGACY_SSE.
C:
WINBASEAPI
PVOID
WINAPI
LocateXStateFeature (
_In_ CONTEXT_EX Context,
_In_ DWORD FeatureId,
_Out_opt_ PDWORD Length
);
Пример использования
Чтобы разобраться с внутренностями XState, особенно в сочетании со структурой данных CONTEXT_EX, мы написали простую тестовую программу, доступную на нашем GitHub здесь (https://github.com/yardenshafir/cet-research/tree/master/ContextEx). Эта утилита демонстрирует некоторые из использованных функций, а также различные смещения, размеры и поведение. Вот выходные данные программы (которая использует регистры AVX) в системе с AVX, MPX и Intel PT:
Помимо прочего, обратите внимание, что прежний CONTEXT находится с отрицательным смещением, как и ожидалось, и как даже если система поддерживает состояние x87 FPU(1) и GSSE(2), XSAVEBV не содержит эти биты, поскольку они вместо этого сохранены в устаревшей области CONTEXT (и, следовательно, обратите внимание на отрицательные смещения связанных с ними данных состояния). После заголовка XSAVE, который составляет 0x40 байтов, обратите внимание, что состояние AVX(2) начинается со смещения 0x70, как и предполагалось в математике.
Проверка структуры CONTEXT_EX
Поскольку функции пользовательского режима могут создавать структуру CONTEXT_EX, которая в конечном итоге обрабатывается ядром и изменяет привилегированные части области XSAVE (а именно, данные состояния CET), Windows должна защищаться от нежелательных изменений, которые могут быть выполнены через функции, которые принимают CONTEXT_EX, такие как:
- NtContinue, которая используется для возобновления работы после исключения, обработки функциональности CRT longjmp, а также выполнения раскрутки стека.
- NtRaiseException, которая используется для вставки исключения в существующий поток
- NtQueueUserApc, которая используется для перехвата потока выполнения существующего потока
- NtSetContextThread, которая используется для изменения регистров/состояния процессора существующего потока
Поскольку любой из этих системных вызовов может заставить ядро изменить либо регистр MSR IA32_PL3_SSP, либо IA32_CET_U, а также напрямую изменить указатель RIP для неожиданной цели, Windows должна проверить, что переданная структура CONTEXT_EX не нарушает гарантии CET.
Вскоре мы расскажем, как это делается для проверки SSP в версии 19H1 и добавления проверки указателя RIP в версии 20H1. Сначала нужно было сделать небольшой рефакторинг, чтобы уменьшить вероятность неправильного использования функции NtContinue: введение функции NtContinueEx.
NtContinueEx и KCONTINUE_ARGUMENT
Как перечислено выше, функциональность функции NtContinue используется в ряде ситуаций, и для обеспечения устойчивости CET в лице функции, которая допускает произвольные изменения состояния процессора, в интерфейс необходимо было добавить более детальное управление. Это было сделано путем создания нового перечисления с именем KCONTINUE_TYPE, которое присутствует в структуре данных KCONTINUE_ARGUMENT, которая теперь должна быть передана в расширенную версию NtContinue — NtContinueEx.
Эта структура данных также содержит новое поле ContinueFlags, которое заменяет исходный аргумент TestAlert для NtContinue на флаг CONTINUE_FLAG_RAISE_ALERT (0x1), а также вводит новый флаг CONTINUE_FLAG_BYPASS_CONTEXT_COPY (0x2), который напрямую доставляет APC с новым APF с с новым TrapFrame. Это оптимизация, которая ранее была реализована путем проверки того, был ли указатель записи CONTEXT в определенном месте в стеке пользователя, что заставило функцию предполагать, что она использовалась как часть доставки APC пользовательского режима. Вызовы, желающие этого поведения, теперь должны явно установить флаг в ContinueFlags.
Обратите внимание, что хотя старый интерфейс по-прежнему поддерживается по устаревшим причинам, он внутренне вызывает функцию NtContinueEx, которая распознает входной параметр как параметр BOOLEAN TestAlert, а не KCONTINUE_ARGUMENT. Такой случай рассматривается как KCONTINUE_UNWIND для целей нового интерфейса.
В рамках этого рефактора существуют следующие четыре возможных типа:
- KCONTINUE_UNWIND - используется старыми вызывающими объектами NtContinue, такими как функция RtlRestoreContext и функция LdrInitializeThunk, которые используется при раскрутки из исключений.
- KCONTINUE_RESUME - используется функцией KiInitializeUserApc при построении структуры KCONTINUE_ARGUMENT в стеке пользовательского режима, в которой KiUserApcDispatcher будет запускаться перед повторным вызовом NtContinueEx.
- KCONTINUE_LONGJUMP - используется функцией RtlContinueLongJump, которая вызывается RtlRestoreContext, если код исключения используется в записи исключения - STATUS_LONGJUMP.
- KCONTINUE_SET - никогда не передается в функцию NtContinueEx напрямую, а используется при вызове KeVerifyContextIpForUserCet из PspGetSetContextInternal в ответ на функцию NtSetContextThread.
Проверка Указателя Теневого Стека (SSP)
Как мы уже упоминали, существуют законные случаи, когда код пользовательского режима должен будет изменить указатель теневого стека, например, раскрутка исключений, APC, longjmp и так далее. Но операционная система должна проверить новое значение, запрошенное для SSP, чтобы для предотвращения обходов CET. В версии 19H1 это было реализовано новой функцией KeVerifyContextXStateCetU. Эта функция получает поток, контекст которого изменяется, и новый контекст для потока и выполняет следующие действия:
- Если структура CONTEXT_EX не содержит никаких данных, XState или если данные XState не содержат регистры CET (проверяется путем вызова RtlLocateExtendedFeature2 с битом состояния XSTATE_CET_U), проверка не требуется.
- Если CET включен в целевом потоке:
* Убедитесь, что вызыв не пытается отключить CET в этом потоке, маскируя флаг XSTATE_MASK_CET_U из XSAVEBV. Если это происходит, функция повторно активирует бит состояния, установит MSR_IA32_CET_SHSTK_EN (который является флагом, который включает функцию теневого стека CET) в Ia32CetUMsr, и установит текущий теневой стек как Ia32Pl3SspMsr.
* В противном случае вызовите функцию KiVerifyContextXStateCetUEnabled, чтобы проверить, включен ли теневой стек CET (включен регистр MSR_IA32_CET_SHSTK_EN), что новый SSP выровнен на 8 байтов и что он находится между текущим значением SSP и концом VAD области теневого стека. Обратите внимание, что поскольку стеки растут в обратном направлении, "конец" области фактически является началом стека. Следовательно, при установке нового контекста для потока любое значение SSP является действительным, если оно находится внутри части теневого стека, который до сих пор использовалась потоком. Нет предела тому, насколько далеко поток может зайти в свой теневой стек.
- Если CET отключен в целевом потоке, и вызов пытается включить его, включив маску XSTATE_CET_U в XSAVEBV в CONTEXT_EX, то разрешить установку обоих значений MSR только в 0 (без стеков теней и без SSP).
Любые сбои в описанных проверках будут возвращать STATUS_SET_CONTEXT_DENIED, тогда как STATUS_SUCCESS возвращается в других случаях.
Включение CET также неявно включает проверку содержимого стека, первоначально реализованную в Windows 8.1 вместе с CFG. Это видно через бит CheckStackExtents в поле ProcessFlags KPROCESS. Это означает, что всякий раз, когда проверяется целевой SSP, также вызывается KeVerifyContextRecord, и функция проверяет, является ли целевой RSP либо частью ограничений пользовательского стека TEB текущего потока (или ограничений пользовательского стека TEB32, если это процесс WOW64). Эти проверки, реализованные функцией RtlGuardIsValidStackPointer (и RtlGuardIsValidWow64StackPointer), ранее были задокументированы (и показаны как недостаточные) исследователями как в Tenable (https://medium.com/tenable-techblog/api-series-setthreadcontext-d08c9f84458d), так и в enSilo (https://blog.ensilo.com/atombombing-cfg-protected-processes).
Проверка указателя инструкций (RIP)
В сборке 19030 появилась другая функция, использующая Intel CET - проверка того, что новый указатель RIP, который пытается установить для процесса, является допустимым. Так же, как проверка SSP, эта защита может быть включена, только если для потока включен CET. Однако проверка указателя RIP не включена по умолчанию и должна быть включена для процесса (что указывается битом UserCetSetContextIpValidation в поле MitigationFlags2Values в EPROCESS).
При этом для текущих сборок создается впечатление, что при вызове CreateProcess и использовании атрибута PROC_THREAD_ATTRIBUTE_MITIGATION_POLICY, если флаг PROCESS_CREATION_MITIGATION_POLICY2_CET_USER_SHADOW_STACKS_ALWAYS_ON включен, эта опция будет установлена. (Обратите внимание, что вызов SetProcessMitgationPolicy со значением ProcessUserShadowStackPolicy недопустим, поскольку CET можно включить только во время создания процесса).
Интересно, однако, что новая опция безопасности была добавлена на карту защиты, PS_MITIGATION_OPTION_USER_CET_SET_CONTEXT_IP_VALIDATION (32). Переключение этого (недокументированного) параметра приводит к включению бита AuditUserCetSetContextIpValidation в поле MitigationFlags2Values, которое будет вскоре описано. Кроме того, поскольку теперь это 32-я опция (каждая из которых занимает 4 бита для DEFERRED/OFF/ON/RESERVED), теперь, таким образом, необходимо 132 бита, и PS_MITIGATION_OPTIONS_MAP расширена до 3 64-битных элементов массива в поле карты.
Новая функция KeVerifyContextIpForUserCet будет вызываться всякий раз, когда собирается изменить контекст потока. Она проверит, что и CET, и защита указателя RIP включены для потока, а также проверит, установлен ли флаг CONTEXT_CONTROL в параметре контекста, означая, что указатель RIP будет изменен этим новым контекстом. Если все эти проверки пройдены, она вызывает внутреннюю функцию KiVerifyContextIpForUserCet. Цель этой функции - проверить, что целевое значение RIP является допустимым значением, а не значением, используемым эксплоитом для запуска произвольного кода.
Сначала она проверяет, что целевой адрес RIP не является адресом ядра, а также не адресом в младших байтах 0x10000, который не должен отображаться. Затем она извлекает этот базовый кадр-ловушку и проверяет, является ли целевой RIP, RIP этого кадра-ловушки. Это предназначено для разрешения случаев, когда целевой RIP является предыдущим адресом в режиме пользователя. Это обычно происходит, когда это первый раз, когда NtSetThreadContext вызывается для этого потока, а RIP устанавливается в качестве начального адреса для потока, но также может происходить и в других, менее распространенных случаях.
Функция получает KCONTINUE_TYPE и, основываясь на его значении, обрабатывает целевой RIP различными способами. В большинстве случаев она будет перебирать теневой стек и искать целевой RIP. Если она не находит его, он будет продолжать работать до тех пор, пока не сработает исключение и не получит обработчик исключений. Обработчик исключений проверит, является ли предоставленный KCONTINUE_TYPE KCONTINUE_UNWIND, и если это так вызывает функцию RtlVerifyUserUnwindTarget с флагом KCONTINUE_UNWIND. Эта функция попытается снова проверить RIP, на этот раз используя более сложные проверки, которые мы опишем в следующем разделе.
В любом другом случае, функция вернет STATUS_SET_CONTEXT_DENIED, что заставит KeVerifyContextIpForUserCet вызвать функцию KiLogUserCetSetContextIpValidationAudit, чтобы проверить сбой, если в AuditUserCetSetContextIpValidationC установлен флаг. Этот "аудит" довольно интересен, так как вместо того, чтобы проводиться по обычному каналу безопасности ETW процессов, он выполняется путем непосредственного вызова исключения быстрого сбоя с помощью службы отчетов об ошибках Windows (WER) (то есть отправки исключения 0xC000409 с информацией установить в FAST_FAIL_SET_CONTEXT_DENIED). Чтобы избежать спама WER, используется другой бит EPROCESS, AuditUserCetSetContextIpValidationLogged.
В одном случае функция прекратит итерацию по теневому стеку, прежде чем найдет целевой указатель RIP - если поток завершается и текущий адрес теневого стека теней выровнен по странице. Это означает, что для завершающих потоков функция будет пытаться проверить целевой RIP только на текущей странице стека теней как "best effort", но не пойдет дальше. Если он не найдет целевой RIP на этой странице, она вернет STATUS_THREAD_IS_TERMINATING.
Другой случай в этой функции - когда KCONTINUE_TYPE имеет значение KCONTINUE_LONGJUMP. Тогда целевой RIP не будет проверен по стеку, но вместо этого будет вызвана функция RtlVerifyUserUnwindTarget с флагом KCONTINUE_LONGJUMP для проверки RIP в таблице longjmp каталога конфигурации загрузки отображений PE. Мы опишем эту таблицу и эти проверки в следующем разделе этого блога.
KeVerifyContextIpForUserCet вызывается одной из следующих двух функций:
- PspGetSetContextInternal - вызывается в ответ на функцию NtSetContextThread.
- KiVerifyContextRecord - вызывается в ответ на API-интерфейсы NtContinueEx, NtRaiseException и, в некоторых случаях, NtSetContextThread. Перед вызовом KeVerifyContextIpForUserCet (только если его полученный ContinueArgument не равен NULL), эта функция проверяет, пытается ли вызывающая сторона изменить регистр CS, и допустимо ли новое значение - процессам, не относящимся к WOW64, разрешено устанавливать CS только в KGDT64_R3_CODE, если только это пико-процессы, в этом случае они могут установить CS в KGDT64_R3_CODE или KGDT64_R3_CMCODE. Любое другое значение заставит KiVerifyContextRecord принудительно установить новое значение CS в KGDT64_R3_CODE. KiVerifyContextRecord вызывается либо KiContinuePreviousModeUser, либо KeVerifyContextRecord. Во втором случае функция проверяет, что RSP находится внутри одного из стеков процессов (native или wow64), и что 64-разрядные процессы будут когда-либо устанавливать CS только в KGDT64_R3_CODE.
Все пути, которые вызывают KeVerifyContextIpForUserCet для проверки целевого RIP, сначала вызывают функцию KeVerifyContextXStateCetU для проверки целевого SSP и выполняют только проверки RIP, если определено, что SSP является действительным.
Раскрутка исключений и проверка Longjmp
Как показано выше, обработка для KCONTEXT_SET и KCONTEXT_RESUME связана с проверкой того, что целевой RIP является частью теневого стека, но другие сценарии (KCONTEXT_UNWIND и KCONTEXT_LONGJMP) требуют расширенной проверки с помощью функции RtlVerifyUserUnwind. Этот второй путь проверки содержит ряд интересных сложностей, которые требовали изменений в формате PE-файла (и поддержки компилятора), а также новый класс информации на уровне ОС, добавленный в NtSetInformationProcess для поддержки компилятора JIT.
Уже добавленный из-за улучшений поддержки Control Flow Guard (CFG), Каталог Конфигурации Загрузки Образа внутри PE-файла теперь содержит информацию для допустимых целей ветвления, используемых как часть пары setjmp/longjmp, которую должен идентифицировать современный компилятор и передать в компоновщик. В CET эти существующие данные используются повторно, но добавляется еще одна таблица и размер для поддержки продолжения обработчика исключений. В то время как Visual Studio 2017 производит таблицу longjmp, только Visual Studio 2019 создает эту более новую таблицу.
В этом последнем разделе мы рассмотрим формат этих таблиц и то, как ядро может авторизовать два последних типа потоков управления KCONTINUE_TYPE.
Таблицы метаданных PE
В дополнение к стандартной таблице GFIDS, присутствующей в образе Control Flow Guard, в Windows 10 также добавлена поддержка проверки целей longjmp путем включения Таблицы Длинных Целевых Переходов , обычно расположенной в разделе PE с именем .gljmp, RVA которого хранится в поле GuardLongJumpTargetTable в каталоге конфигурации загрузки изображений.
Всякий раз, когда в коде делается вызов setjmp, в эту таблицу добавляется RVA обратного адреса (в который будет переходить longjmp). Наличие этой таблицы определяется флагом IMAGE_GUARD_CF_LONGJUMP_TABLE_PRESENT в GuardFlags каталога конфигурации загрузки образа и содержит столько записей, сколько указано в поле GuardLongJumpTargetCount.
Каждая запись представляет собой 4-байтовый адрес RVA плюс n байтов метаданных, где n берется из результата (GuardFlags & IMAGE_GUARD_CF_FUNCTION_TABLE_SIZE_MASK) >> IMAGE_GUARD_CF_FUNCTION_TABLE_SIZE_SHIFT. Для этой таблицы метаданные не определены, поэтому ожидается, что байты метаданных всегда будут равны нулю. Интересно, что, поскольку этот расчет такой же, как и для таблицы GFIDS (которая может содержать метаданные, если включено подавление экспорта), подавление хотя бы одной цели CFG приведет к добавлению 1 байта пустых метаданных к каждой записи в Long Jump Target Table.
Например, вот PE-файл с двумя целями longjmp:
Обратите внимание на значение 1 в верхнем полубайте GuardFlags (что соответствует IMAGE_GUARD_CF_FUNCTION_TABLE_SIZE_MASK) из-за того, что этот образ также использует подавление экспорта CFG. Это говорит нам о том, что один дополнительный байт метаданных будет присутствовать в Long Jump Target Table, которую вы можете увидеть ниже:
В Windows 10 версии 20H1 метаданные этого типа теперь включены в одну дополнительную ситуацию - когда цели продолжения обработчика исключений присутствуют как часть потока управления двоичного файла. Два новых поля - GuardEHContinuationTable и GuardEHContinuationCount - добавляются в конец каталога конфигурации загрузки образа, и флаг IMAGE_GUARD_EH_CONTINUATION_TABLE_PRESENT теперь является частью GuardFlags. Структура этой таблицы идентична той, которая показана для Long Jump Target Table включая добавление байтов метаданных, основанных на верхнем полубайте GuardFlags.
К сожалению, даже текущие предварительные версии Visual Studio 2019 не генерируют эти данные, поэтому в настоящее время мы не можем показать вам пример - этот анализ основан на реверсинге кода проверки, который мы опишем позже, а также заголовочного файла Ntimage.h в 20H1 SDK.
Таблица обращенных пользователем функций
Теперь, когда мы знаем, что изменения потока управления могут произойти для перехода к цели longjmp или цели продолжения обработчика исключений, возникает вопрос - как мы можем получить эти две таблицы на основе RIP-адреса, присутствующего в структуре CONTEXT_EX, как части вызова функции NtContinueEx? Поскольку эти операции могут часто происходить в контексте выполнения некоторых программ, ядру необходим эффективный способ решения этой проблемы.
Возможно, вы уже знакомы с концепцией таблицы Инвертированных Функций. Такая таблица используется библиотекой Ntdll.dll (LdrpInvertedFunctionTable) для поиска кодов операций раскрутки и данных исключений во время обработки исключений в пользовательском режиме (например, путем поиска раздела .pdata). Другая таблица присутствует в файле Ntoskrnl.exe (PsInvertedFunctionTable) и используется во время обработки исключений режима ядра, а также в качестве части проверок PatchGuard.
Короче говоря, Таблица Инвертированных Функций - это массив, содержащий все загруженные модули пользователя/ядра по их размеру и указатель на каталог исключений PE, отсортированный по виртуальному адресу. Первоначально он был создан как оптимизация, поскольку поиск в этом массиве выполняется намного быстрее, чем синтаксический анализ заголовка PE, а затем поиск в связанном списке загруженных модулей - бинарный поиск в таблице инвертированных функций быстро найдет любой виртуальный адрес в соответствующем модуле только как log(n). Ken Johnson и Matt Miller, ныне известные Microsoft, ранее опубликовали подробный обзор в рамках своей статьи о методах перехвата в режиме ядра в журнале Uninformed.
Ранее, однако, библиотека Ntdll.dll только сканировал свою таблицу на предмет исключений пользовательского режима, а ядро Ntoskrnl.exe только сканировало свой аналога на предмет исключений режима ядра - изменения в версии 20H1 заключаются в том, что ядру теперь придется сканировать также пользовательскую таблицу - как часть новой логики, необходимой для обработки longjmp и исключений. Для поддержки этого добавлена новая функция RtlpLookupUserFunctionTableInverted, которая сканирует переменную KeUserInvertedFunctionTable, сопоставляя ее с теперь экспортированным символом LdrpInvertedFunctionTable в Ntdll.dll.
Это захватывающая криминалистическая возможность, так как она означает, что теперь у вас есть простой способ найти модули пользовательского режима, загруженные в текущем процессе, без необходимости разбирать данные загрузчика PEB или перечислять VAD. Например, вот как вы можете видеть текущие загруженные образы в процессе Csrss.exe:
Bash:
dx @$cursession.Processes.Where(p => p.Name == "csrss.exe").First().SwitchTo()
dx -r0 @$table = *(nt!_INVERTED_FUNCTION_TABLE**)&nt!KeUserInvertedFunctionTable
dx -g @$table->TableEntry.Take(@$table->CurrentSize)
При этом существует, хотя и удаленная, возможность того, что образ не содержит каталог исключений, особенно в системах x86, где не существует опкодов расскрутки, и секция .pdata создается только в том случае, если используется /SAFESEH и существует хотя бы один обработчик исключений.
В этих ситуациях RtlpLookupUserFunctionTableInverted может завершиться ошибкой, и вместо этого необходимо использовать функцию MmGetImageBase. Неудивительно, что при этом ищется любой VAD, который отображает регион, соответствующий входному указателю RIP, и, если это образ VAD, возвращает базовый адрес и размер региона (который должен соответствовать адресу модуля).
Цели обработчика динамических исключений
Последнее препятствие существует при обработке запросов KCONTINUE_UNWIND - хотя обычные процессы имеют в своих кодах цели продолжения обработчика статических исключений, основанные на предложениях __try/__except/__ finally, Windows позволяет механизмам JIT не только динамически создавать исполняемый код на лету, но и зарегистрировать обработчики исключений (и раскрутить опкоды) для него во время выполнения, например, через вызов RtlAddFunctionTable. Хотя эти обработчики исключений ранее были нужны только для обхода стека в пользовательском режиме и расскрутки исключений, теперь обработчики продолжения становятся законными целями потока управления, которые ядро должно понимать как потенциально допустимые значения для RIP. Это последняя возможность, которую обрабатывает RtlpFindDynamicEHContinuationTarget.
В рамках поддержки CET и введения NtContinueEx структура EPROCESS была расширена двумя новыми полями, названными DynamicEHContinuationTargetsLock и DynamicEHContinuationTargetsTree, первое из которых является EX_PUSH_LOCK, а второе - RTL_RB_TREE, которое содержит все действительные адреса обработчиков исключений. Это дерево управляется посредством вызова NtSetInformationProcess с новым классом информации о процессе, ProcessDynamicEHContinuationTargets, которая сопровождается структурой данных типа PROCESS_DYNAMIC_EH_CONTINUATION_TARGETS_INFORMATION, содержащий, в свою очередь массив записей PROCESS_DYNAMIC_EH_CONTINUATION_TARGET, которые будут проверены перед изменением DynamicEHContinuationTargetsTree. Чтобы упростить задачу, смотри определения ниже для этих структур и флагов:
C:
#define DYNAMIC_EH_CONTINUATION_TARGET_ADD 0x01
#define DYNAMIC_EH_CONTINUATION_TARGET_PROCESSED 0x02
typedef struct _PROCESS_DYNAMIC_EH_CONTINUATION_TARGET
{
ULONG_PTR TargetAddress;
ULONGLONG Flags;
} PROCESS_DYNAMIC_EH_CONTINUATION_TARGET, *PPROCESS_DYNAMIC_EH_CONTINUATION_TARGET;
typedef struct _PROCESS_DYNAMIC_EH_CONTINUATION_TARGETS_INFORMATION
{
USHORT NumberOfTargets;
USHORT Reserved;
ULONG Reserved2;
PPROCESS_DYNAMIC_EH_CONTINUATION_TARGET* Targets;
} PROCESS_DYNAMIC_EH_CONTINUATION_TARGETS_INFORMATION, *PPROCESS_DYNAMIC_EH_CONTINUATION_TARGETS_INFORMATION;
Функция PspProcessDynamicEHContinuationTargets вызывается для итерации по этим данным, в какой момент RtlAddDynamicEHContinuationTarget вызываются для любого элемента, содержащего установленный флаг DYNAMIC_EH_CONTINUATION_TARGET_ADD, который выделяет структуру данных, хранящую целевой адрес, и связывая его RTL_BALANCED_NODE связь с RTL_RB_TREE в EPROCESS. И наоборот, если флаг отсутствует, то цель ищется, и если она действительно существует, удаляется и ее узел освобождается. По мере обработки каждой записи флаг DYNAMIC_EH_CONTINUATION_TARGET_PROCESSED вставляется в исходный буфер ввода OR, чтобы вызовы могли знать, какие записи работали, а какие — нет.
Очевидно, что существование этой возможности является универсальным обходом любой CET/CFG-подобной возможности, поскольку каждый возможный гаджет ROP может быть просто добавлен как "цель динамического продолжения". Однако, поскольку Microsoft в настоящее время только законно поддерживает JIT-компиляцию вне процесса для браузеров и Flash, важно отметить, что этот API работает только для удаленных процессов. Фактически, вызов этого в текущем процессе всегда будет неудачным со статусом STATUS_ACCESS_DENIED.
Целевая проверка
Объединяя все эти знания вместе, функцию RtlVerifyUserUnwindTarget становится довольно легко объяснить.
- Найдите загруженный PE-модуль, связанный с целевым указателем RIP, в структуре CONTEXT_EX. Сначала попробуйте использовать RtlpLookupUserFunctionTableInverted и, если это не получится, переключитесь на использование функции MmGetImageBase, убедившись, что модуль <4 ГБ.
- Если модуль был найден, вызовите функцию LdrImageDirectoryEntryToLoadConfig, чтобы получить его Каталог Конфигурации Загрузки Образа. Затем убедитесь, что он достаточно большой, чтобы содержать Long Jump or DynamicExceptionHandlerContinuationTarget и чтобы защитные флаги содержали IMAGE_GUARD_CF_LONGJUMP_TABLE_PRESENT или IMAGE_GUARD_EH_CONTINUATION_TABLE_PRESENT. Если каталог отсутствует, слишком мал или отсутствует соответствующая таблица, верните STATUS_SUCCESS из соображений совместимости.
-Получите GuardLongJumpTargetTable или GuardEHContinuationTable из Каталога Конфигурации Загрузки Образа и проверьте GuardLongJumpTargetCount или GuardEHContinuationCount. Если существует более 4 миллиардов записей, верните STATUS_INTEGER_OVERFLOW. Если записей больше 0, то вызовите бинарный поиск, используя функцию bsearch_s (передавая RtlpTargetCompare в качестве компаратора) через таблицу, чтобы найти целевой RIP после преобразования его в RVA. Если он найден, то вернутся STATUS_SUCCESS.
- Если целевой RIP не был найден (или если таблица содержала 0 записей для начала), или если загруженный модуль не был найден в целевом RIP в первую очередь, тогда верните STATUS_SET_CONTEXT_DENIED для проверок longjmp (KCONTINUE_LONGJUMP).
- В противном случае, для проверки раскуртки исключений (KCONTINUE_UNWIND) вызовите RtlpFindDynamicEHContinuationTarget, чтобы проверить, была ли это зарегистрированная цель продолжения обработчика динамических исключений. Если да, верните STATUS_SUCCESS, в противном случае верните STATUS_SET_CONTEXT_DENIED.
Заключение
Внедрение CET и связанные с ним меры по безопасности являются важным шагом на пути к устранению использования ROP и других методов перехвата потока управления. Целостность потока управления, очевидно, является сложной темой, которая, вероятно, станет еще более сложной, поскольку в будущем к ней будут добавлены дополнительные средства защиты. Дальнейшие проблемы совместимости и одноразовые сценарии, вероятно, приведут к тому, что будет обнаружено все больше и больше случаев, которые потребуют особой обработки. Тем не менее, такой большой шаг в технологии защиты, особенно такой, который включает в себя так много новых функций, неизбежно будет иметь пробелы и проблемы, и мы уверены, что по мере проведения дополнительных исследований в этой области, интересные вещи будут обнаружены там и в будущее.
источник https://windows-internals.com/cet-on-windows/
Перевод: yashechka, специально для https://xss.pro
Последнее редактирование модератором: