В этом посте объясняется, как Meltdown все еще можно использовать для получения некоторых специфических данных ядра и взлома Windows KASLR в последних версиях Windows, включая "Windows 10" 20H1, несмотря на механизм KVA Shadow, введенный для предотвращения исполнения атаки Meltdown.
В начале 2018 года, была публично представлена одна из самых известных уязвимостей в истории: Meltdown (https://meltdownattack.com/). Хотя это была уязвимость центрального процессора, большинство операционных систем, включая Windows, добавили меры по защиты от этой атаки. Несмотря на то, что эти меры для Windows были эффективны для предотвращения Meltdown, они также привели к проблеме "дизайна", которой можно злоупотреблять даже сегодня.
Обратите внимание, что с Microsoft связались по этой проблеме, и их позиция заключается в том, что защита KVA Shadow не предназначена для защиты KASLR. Они специально указали на этот проектный документ ( https://msrc-blog.microsoft.com/2018/03/23/kva-shadow-mitigating-meltdown-on-windows/), и в частности на эту часть документа:
"Обратите внимание, что одним из следствий этого выбора конструкции является то, что Shadow KVA не защищает от атак на ASLR ядра с использованием спекулятивных побочных каналов. Это преднамеренное решение, учитывая сложность конструкции Shadow KVA, временные рамки и реалии других проблем с побочными каналами, влияющих на те же конструкции процессора. Примечательно, что процессоры, подверженные мошеннической загрузке на кэш данных, также обычно подвержены другим атакам на их BTB (целевые буферы ветвления) и другим микроархитектурным ресурсам, которые могут позволить раскрытие макета адресного пространства ядра локальному злоумышленнику, который выполняет произвольный собственный код".
В этом посте будет рассказано, как Meltdown все еще можно использовать для получения некоторых конкретных данных ядра и взлома Windows KASLR в последних версиях Windows, включая "Windows 10" 20H1.
Атака Meltdown
Атака Meltdown состояла в дампинге данных с произвольных адресов ядра, которые можно использовать для получения конфиденциальных данных, таких как (например, учетные данные), или использовать эксплоиты ядра, чтобы сделать эксплуатацию проще и надежнее.CVE, присвоенный Meltdown, был CVE-2017-5754 (https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2017-5754), который был представлен как уязвимость "Rogue Data Cache Load".
Meltdown была представлена одновременно со второй уязвимостью, обнаруженной теми же группами исследователей безопасности: Spectre. Кроме того, было обнаружено, что процессоры AMD не были затронуты Meltdown, но были затронуты Spectre.
Важно уточнить, что уязвимость Meltdown можно исправить с помощью обновления микрокода центрального процессора (не очень часто) или просто с помощью новейших моделей процессоров Intel на основе микроархитектуры "Cascade Lake" (смотри https://en.wikipedia.org/wiki/Cascade_Lake_(microarchitecture)).
Детали Meltdown - Часть 1
Эта атака возможна из-за состязания между доступом к памяти и проверкой привилегий в спекулятивном механизме выполнения, который позволяет считывать данные ядра из кода, работающего в пользовательском режиме. Обычно это невозможно из-за разрешений между памятью пользователя и ядра. Однако, Meltdown показала, что это все еще возможно, злоупотребляя побочными эффектами спекулятивных вычислений.
На очень высоком уровне, спекулятивное выполнение выполняется процессором, когда он не знает результатов какой-либо операции (например, доступ к памяти). Вместо того, чтобы ждать завершения операции, центральный процессор "спекулирует" результатами и продолжает выполнение на основе этой спекуляции. Когда результаты известны, предполагаемый код либо фиксируется (то есть его результаты становятся видимыми), либо просто отбрасывается.
В лучшем случае центральный процессор выполнил работу заранее, а в худшем - потратил немного вычислительной мощности вместо ожидания.
Прежде чем понять эту атаку, я (как и большинство людей) предположил, что эффекты спекулятивного выполнения были невидимы для выполняемого кода. Однако это оказалось далеко от истины ..
Чтобы понять, что мы подразумеваем под этим, давайте начнем с анализа примера, аналогичного примеру оригинальной статьи, но с использованием C вместо ассемблера:
А теперь давайте предположим, что:
Из быстрого анализа единственный способ достичь условия что pos = 0, где позиция 0 массива array2 читается, а результат используется в качестве индекса для чтения array1. Выходные данные затем присваиваются значению temp_value.
Другими словами, любое значение pos больше 0 не достигнет кода внутри условия if.
Теперь давайте представим, что эта функция выполняется спекулятивно, где условие if предполагается истинным для произвольного значения pos:
В этом случае любая позиция array2 может быть прочитана, а затем загруженный байт будет использоваться как индекс для чтения одного элемента array1. Если значение pos слишком велико, его можно использовать для чтения памяти ядра относительно базового адреса array2, например:
Или лучше, прочитав один байт по произвольному адресу следующим образом:
Проблема здесь в том, что этот код внутренне выполняется центральными процессором, а затем отбрасывается, потому что предположение было неверным. Таким образом, мы не можем видеть содержимое загруженного байта array2[pos], или мы можем?
Детали Meltdown - Часть 2
Вот где появляется вторая часть атаки Meltdown, в которой первая часть (уязвимость "Rogue Data Cache Load") сочетается с атакой в побочный канал кэша. Мы сказали, что невозможно увидеть результат array2[pos], но его содержимое сразу же используется как индекс в array1 [unknown]. Итак, хитрость заключается в том, чтобы определить, к какому элементу array1 мы обращались.
Хотя это может показаться на первый взгляд не очень интуитивным, но можно измерить время доступа для определенных областей памяти, таких как переменные или элементы массивов, из самого кода. Поскольку процессоры используют кеши для хранения содержимого недавно использованной памяти, а кеши значительно быстрее, чем обычная память, мы получаем возможность определить, к какому элементу обращались, наблюдая время доступа к различным строкам кеша.
Типичный способ определить, к какой строке кэша был произведен доступ, - сначала очистить все строки кэша, затем вызывать доступ к памяти жертвы, а затем синхронизировать доступ к каждой строке кэша. Линия, к которой имелся доступ жертвы, тогда произведет самое низкий значение тайминга. Этот метод обычно известен как Flush + Reload.
Мы можем очистить строки кэша для всех элементов массива измерений с помощью встроенной функции _mm_clflush:
После этого, мы выполняем функцию, упомянутую выше (leaker_function), активируя спекулятивное выполнение. На последнем шаге, необходимо знать, к какому элементу массива обращались. Для этого, мы просто читаем каждый элемент array1 и измеряем время доступа, используя встроенную функцию __rdtscp.
Давайте рассмотрим пример, где pos = 0xffff8000'12345678 - &array2, что означает, что мы собираемся прочитать адрес ядра 0xffff8000'12345678:
И давайте предположим, что содержимое array2 [0xffff8000'12345678 - & array2] равно 0x41, поэтому происходит следующий доступ:
Таким образом, когда код выполняется спекулятивно, доступ к позиции 0x41 массива1 будет осуществляться, и центральный процессора будет сохранять в кэше позицию 0x41 array1. При проверке элемента за элементом массива, время доступа для этой позиции должно быть меньше, чем для всех других позиций. Если это произойдет, мы можем предположить, что значение загруженного байта было 0x41, что означает, что содержимое адреса ядра 0xffff8000'12345678 равно 0x41.
Подводя итог, можно предпринять следующие шаги для реализации атаки Meltdown:
Конечно, это ограниченное объяснение того, как на самом деле работает атака, и нужно учитывать и другие вещи, но этого должно быть достаточно для понимания общих принципов.
Детали Meltdown - Часть 3
К этому времени, вопрос состоит в том, как мы разрешаем спекулятивное исполнение? Один из способов сделать это - создать код, который выполняет некоторые "обфусцированные" вычисления, а затем поместить его в состояние цикла. Когда центральный процессор обнаруживает этот повторяющийся код, активируется выполнение "вне очереди", которое пытается оптимизировать выполнение путем нахождения всех возможных путей выполнения, допустимых или "вероятно" допустимых. В зависимости от выбранного пути выполнения центральный процессор может заранее прочитать память.
Когда поток выполнения наконец достигает условия чтения этой памяти, время доступа должно быть быстрее, потому что он был кэширован центральным процессором ранее. Используя в качестве вдохновения PoC, расположенный здесь (https://github.com/deeptechlabs/meltdown/blob/master/src/poc.c), мы можем увидеть способ запустить спекулятор и получить утечку памяти ядра, как показано ниже:
Детали Meltdown - некоторые соображения
Очень важно сказать, что для возможности получения данных ядра из пользовательского режима с помощью этой атаки данные должны быть кэшированы центральным процессором, иначе нет никакой возможности что-либо получить. По этой причине, злоумышленник должен найти способ кеширования целевых данных, вызывая API ядра или просто вызывая исключения, но следя за тем, чтобы эти данные постоянно использовались процессором.
Еще одна важная вещь: процессор обычно использует строку кэша размером 64 байта (64-байтовый блок кэша), что означает, что, если мы сможем заставить процессор кэшировать содержимое адреса X ядра, будет загружена вся строка кэша, что даст нам возможность получить любой байта из этого 64-байтового диапазона, не ограничивая утечку одним байтом.
Страничные таблицы Intel
Когда AMD выпустила 64-разрядный процессор, основанный на наборе команд Intel x86, адресуемая виртуальная память была увеличена с 2^32 (4 гигабайта) до 2^48 (256 терабайт). Хотя центральный процессор работает в 64-битном режиме, только 48 бит используются для адресации виртуальной памяти (канонические адреса, где верхние 17 битов равны 0 или 1). Для этого процессор пропускает 16 бит виртуального диапазона адресов, разделяя виртуальную память на две части и помещая большую дыру в середину (неканонические адреса).
Диапазоны виртуальной памяти:
Чтобы отобразить 48 бит виртуальной памяти, процессор увеличил уровни подкачки с 2 до 4.
От самого высокого до самого низкого уровня имена таблиц подкачки:
На этих четырех уровнях подкачки каждая таблица имеет 512 записей (0x200), где каждая запись занимает 8 байтов. Таким образом, размер каждой таблицы составляет 0x200 * 8 = 0x1000 (4 КБ).
Нумерация от низшего к высшему уровню:
Наконец, если мы умножим записи PML4 на 512 ГБ, мы сможем получить полностью адресуемую виртуальную память:
На следующем рисунке показана 64-битная модель уровня подкачки:
Из обзора видно, что каждая запись на каждом уровне подкачки имеет одинаковый формат: она разделена на две части: разрешения и физический адрес (PFN), указанные в записи.
Среди наиболее важных битов защиты можно упомянуть:
Где значение каждого бита следующее:
Если бит PS включен, нижние уровни подкачки игнорируются, и в зависимости от уровня, на котором он включен, размер страницы может составлять 2 МБ или 1 ГБ.
Примечание. До появления 64-разрядных процессоров Intel для 32-разрядных процессоров использовались трехуровневые системы подкачки, позволяющие обрабатывать до 64 ГБ физической памяти с помощью поддержки PAE (расширение физических адресов), где таблица PDPT находилась на самом высоком уровне.
Патч Windows Meltdown
3 января 2018 года Microsoft выпустила исправление для уязвимости Meltdown (https://portal.msrc.microsoft.com/en-US/security-guidance/advisory/ADV180002 ). Оно было названо KVA Shadow (https://msrc-blog.microsoft.com/2018/03/23/kva-shadow-mitigating-meltdown-on-windows/), которое было основано на решении KPTI (Kernel Page Table Isolation) (https://en.wikipedia.org/wiki/Kernel_page-table_isolation), реализованном в Linux. По сути, это патч состоит в том, что большинство страниц памяти ядра отключается при выполнении в пользовательском режиме, чтобы уменьшить поверхность атаки. Таким образом, атака Meltdown перестает работать, в основном потому, что данные не утекают, даже если процессор уязвим.
Защита строиться на использовании двух разных таблиц подкачки: одна для выполнения в режиме ядра и одна для выполнения в пользовательском режиме. Таблица PML4, используемая для режима ядра, заполнена, поэтому память пользователя и ядра полностью отображается. Напротив, таблица PML4, используемая для пользовательского режима, представляет собой так называемый Shadow PML4, который отображает всю пользовательскую память и только небольшую часть памяти ядра.
На скриншотах ниже мы можем видеть полную PML4 и теневую PML4, где много записей отсутствуют.
Полная таблица PML4
Теневая таблица PML4
Переход между теневой и полной таблицами подкачки выполняется в режиме ядра, когда пользовательский процесс вызывает системный вызов или просто создает исключение (например, деление на ноль, недопустимая память и так далее).
Примечание: Код, работающий на высоком уровне целостности, например приложения, запускаемые с помощью параметра "Запуск от имени администратора", не использует теневую таблицу PML4.
Windows SMEP с помощью программного обеспечения
Аппаратная поддержка SMEP (Supervisor Mode Execution Prevention) была представлена в микроархитектуре Intel Ivy Bridge (https://en.wikipedia.org/wiki/Ivy_Bridge_(microarchitecture)). По сути, эта функция была добавлена, чтобы избежать выполнения кода пользовательского пространства в режиме ядра, что часто использовалось в прошлом для эксплоитов ядра. Эта функция присутствует на большинстве современных компьютеров в настоящее время.
Несмотря на эту аппаратную защиту и использование двух разных таблиц PML4 для реализации защиты от последствий Meltdown, Microsoft решила воспользоваться этим, добавив улучшение безопасности путем внедрения SMEP с помощью программного обеспечения.
На следующем рисунке мы видим записи пользователя из полной таблицы PML4:
Глядя на красные метки, мы видим, что пользовательские записи таблицы PML4 устанавливаются с битом XD (смотри https://en.wikipedia.org/wiki/Executable_space_protection), что означает, что весь пользовательский код НЕ исполняется в режиме ядра. Таким образом, компьютеры со старыми моделями центрального процессора должны быть защищены от эксплоитов ядра, как если бы присутствовала функция SMEP. В двух словах, любой эксплоит ядра, который пытается выполнить пользовательский код, потерпит крах.
Важно уточнить, что эта аппаратная защита SMEP может быть напрямую обойдена с использованием метода, описанного в презентации "Обход SMEP Windows: U = S" (https://www.coresecurity.com/core-labs/publications/windows-smep-bypass-us) или просто отключив эту функцию из регистра CR4 (20-й бит). С программной реализацией SMEP можно использовать те же методы, но необходимо добавить дополнительный шаг, отключив бит XD в записи таблицы PML4, связанной с целевым виртуальным адресом.
PML4 в Windows до рандомизации
Как упоминалось ранее, таблица PML4 является самым высоким уровнем подкачки в 64-битных моделях памяти. В Windows эта таблица используется для разделения памяти пользователя и ядра, просто используя самые низкие записи 0x100 (256) для отображения пользовательской памяти и самые высокие записи 0x100 для отображения памяти ядра. Зная, что каждая запись PML4 может отображать 512 ГБ (0x8000000000 байт) и для режима пользователя назначено 0x100 записей, мы можем вычесть диапазон виртуальных адресов пользователя, просто выполнив следующие вычисления:
Который так же, как:
Поскольку специальная техника, используемая Windows для управления пейджингом, называемая "self-referential", которая состоит из записи таблицы PML4, указывающей на себя (указывающей на таблицу PML4, которая содержит саму запись), виртуальные адреса для всей таблицы пейджинга могут быть рассчитывается просто зная адрес таблицы PML4. В то же время адрес PML4 можно рассчитать, просто зная, какая запись PML4 использовалась ядром Windows в качестве "self-referential" после загрузки.
Ниже приведен скриншот таблицы PML4, где можно увидеть ссылку на self-referential запись
Первоначально, эта таблица всегда размещалась по одному и тому же адресу ядра, потому что запись, которая раньше делала это, находилась в фиксированной позиции таблицы, расположенной в записи 0x1ED. Исходный адрес Windows таблицы PML4 можно рассчитать, выполнив следующие вычисления:
Адрес 0xFFFFF6FB7DBED000 использовался во всех версиях Windows, пока он не был рандомизирован.
PML4 в Windows после рандомизации
Рандомизация таблиц подкачки Windows была введена в "Windows 10" v1607 (RS1 - "Anniversary Update"), которая была выпущена в 2016 году. Так как он все еще использует технику self-referential, рандомизация была ограничена 256 позициями в таблице PML4, что означает, что таблица может быть распределена только по 256 различным адресам ядра, то есть действительно это плохая рандомизация. Как описано в предыдущем разделе, адрес таблицы PML4 можно рассчитать, зная, какая запись таблицы PML4 является self-referential.
Если мы хотим знать все возможные адреса PML4, мы можем сделать что-то вроде следующего:
Так как эта таблица была рандомизирована, и никакая Windows API не может сказать нам, где эта таблица расположена, злоупотребление таблицами подкачки эксплоитами ядра уменьшалось.
PML4 в реальных сценариях эксплуатации
Использование таблиц подкачки и злоупотребление ими при использовании эксплоитов ядра не очень популярны, скорее всего, из-за необходимости хорошего понимания системы подкачки. Действительно, использование содержимого памяти для отображения другой памяти не является тривиальной концепцией.
С другой стороны, модификации таблиц подкачки могут использоваться для создания примитивов, таких как произвольное чтение/запись ядра и даже выполнение кода ядра. Кроме того, они могут выполняться даже эксплоитами ядра, работающими на любом уровне целостности, включая Low IL. Это делает их очень мощным способом уйти от самых сложных реализаций песочницы, таких как процесс рендеринга Chrome.
Некоторые методы, основанные на злоупотреблении таблицами подкачки, перечислены ниже:
В зависимости от метода, упомянутого выше, может потребоваться включить только несколько битов для создания правильной записи в таблице подкачки, что означает, что это действительно полезно для большинства условий write-what-where, возникающих в эксплойтах ядра.
PML4 экспозиция в Windows после исправления Meltdown
На этом этапе, необходимо сказать, что исправление Windows Meltdown имеет проблему "проектирования", когда не все данные, чувствительные к ядру, скрыты от пользовательского режима. Как упоминалось ранее, две разных таблицы PML4 используются для защиты от атаки Meltdown, которая используется процессором PML4 shadow при работе в пользовательском режиме.
И здесь возникает проблема: теневая таблица PML4 отображается в теневую память ядра, что означает, что она подвергается воздействию пользовательского режима даже при реализованном защите от Meltdown. Таким образом, и как следствие техники self-referential entry, все отображенные таблицы подкачки теневой таблицы PML4 могут быть получены с помощью атаки Meltdown!
Очень важно уточнить, что, хотя правильная настройка таблицы PML4 имеет решающее значение для работающей реализации виртуальной памяти, на самом деле нет необходимости отображать ее в виртуальной памяти или, по крайней мере, не отображать ее постоянно, а просто отображать, когда это необходимо. Ясно, что в некоторых случаях, когда пользовательская память отображается пользовательским кодом, ядру Windows приходится обновлять как теневую, так и всю PML4.
Это обновление выполняется в режиме ядра, где используется полная таблица PML4, а не теневая. Итак, поскольку теневая таблица PML4 представляет собой только блок памяти размером 4 КБ, эта таблица может отображаться в любой части пространства ядра, как и любое другое выделение памяти, без предоставления ее пользовательскому режиму.
Причина отображения таблиц подкачки в теневой таблицы PML4 не совсем ясна, за исключением проблем с производительностью между переключениями контекста между теневым и полным режимом и наоборот. На следующем снимке экрана мы можем видеть, как атака Meltdown способна сбросить первую часть таблицы PML4 в последней версии "Windows 10" (20H1), которая связана с диапазоном виртуальных адресов 0 ~ 0x380'00000000 (0 ~ 7GB):
Конечно, чтобы иметь возможность получить данных, показанных на рисунке выше, необходимо знать, где расположены таблицы подкачки.
Дерандомизация PML4 через Meltdown
В секции PML4 в Windows после рандомизации, описанном выше, мы видим, что рандомизация таблицы PML4 плохая, и она может принимать только 256 разных адресов. Кроме того, взглянув на предыдущий раздел, мы можем увидеть, что можно использовать утечку данных из таблиц подкачки с помощью атаки Meltdown.
Наконец, флаги разрешений, используемые self-referential entry, представлены значением 0x63 (Dirty, Accessed, Writable и Present), а его смещение в PML4 определяется адресом самого PML4. Таким образом, нахождение записи с этим байтом, установленным в 0x63 с правым смещением, с помощью Meltdown позволяет найти таблицу PML4. Собрав все это вместе, мы можем сделать вывод, что можно узнать, где расположена таблица PML4, просто выполнив следующий код:
На следующем снимке экрана мы можем видеть, как адрес этой таблицы дерандомизируется с помощью Meltdown за 16 миллисекунд.
Код ликера PML4 можно скачать здесь (https://github.com/bluefrostsecurity/Meltdown-KVA-Shadow-Leak/).
Дерандомизация PML4 (выводы)
Вышеописанная дерандомизация обрекает усилия Microsoft по рандомизации таблиц подкачки, делая их снова предсказуемыми. Поскольку адрес PML4 не может быть получен путем вызова какого-либо API-интерфейса Windows, этот метод действительно полезен для любого эксплоита повышения привилегий ядра, работающего на низком или среднем уровне целостности.
Важно отметить, что до появления Meltdown еще одна техника дерандомизации PML4 была представлена на Ekoparty в лекции "Я знаю, где живет ваша страница: удаление рандомизации ядра Windows 10" (видео), которую дал Enrique Nissim (@kiqueNissim) в 2016 году.
NT утечка через Meltdown (вступление)
С появлением исправления Meltdown появилась группа новых функций NT. Эти функции были добавлены, чтобы иметь возможность обрабатывать системные вызовы и пользовательские исключения, когда эта защита включена. Все эти новые функции имеют имена, похожие на оригинальные, за исключением простого добавления к имени постфикса Shadow. Например, функция деления на нулевое исключение изначально называлась KiDivideErrorFault, а теперь — KiDivideErrorFaultShadow.
Эти функции Shadow являются просто обертками вокруг оригинальных, где выполняется переключение контекста с теневого на полные таблицы подкачки. На следующем снимке экрана мы видим, как загружаются полные таблицы подкачки при установке регистра CR3:
Весь этот код был помещен в файловый раздел с именем KVASCODE, который находится в модуле ntoskrnl.exe, где размер раздела составляет 0x3000 байт (3 страницы). Поскольку для поддержания работоспособности ОС должны присутствовать системные вызовы и исключения, необходимо сопоставить их с теневой стороной, что означает, что они должны быть доступны пользовательскому режиму. Если записи таблицы подкачки могут быть прочитаны с помощью Meltdown, имеет смысл подумать, что возможно утечка местоположения, где также отображается этот теневой код.
Утечка NT через Meltdown
Как объяснялось выше, 64-разрядная модель памяти Intel использует четыре уровня таблицы подкачки, где обычно для отображения виртуальной памяти используется самый низкий уровень (ТАБЛИЦА СТРАНИЦ). Чтобы определить местонахождение теневого кода, необходимо определить, какие PTE используются для сопоставления этого раздела кода. Поскольку это исполняемый раздел, соответствующие PTE отключили бит XD, что упрощает идентификацию теневого кода.
Таким образом, используя Meltdown, который дает нам возможность читать содержимое таблиц подкачки, можно найти этот код, обработав четыре уровня подкачки, начиная с теневого PML4 и переходя на следующий более низкий уровень, когда появляется действительная запись. Если этот процесс повторяется и обнаруживаются три последовательных исполняемых PTE, это означает, что был найден теневой код. Используя Meltdown, это можно сделать, выполнив следующие шаги:
Как только этот код раздела найден, последним шагом является вычитание дельта-смещения из раздела KVASCODE в базовый адрес "ntoskrnl.exe", как показано ниже:
Поскольку этот теневой код является частью NT, получение базового адреса - это просто вычитание. Важно уточнить, что смещение (дельта) этого раздела изменяется между выпусками Windows, но обычно оно одинаково для разных версий ntoskrnl.exe одного и того же выпуска. В случае Windows 20H1 "Обновление от мая 2020 года" смещение от базового адреса NT составляет 0xa21000 байт, что соответствует страницам 0xa21.
На следующем снимке экрана мы можем видеть, как базовый адрес ntoskrnl.exe может быть получен за 900 миллисекунд:
Ликер NT можно скачать здесь. (https://github.com/bluefrostsecurity/Meltdown-KVA-Shadow-Leak/)
Примечание. Начиная с Windows 10 RS6, при работе на компьютерах с оперативной памятью, равной или превышающей 4 ГБ, базовый адрес ntoskrnl.exe выравнивается до 2 МБ.
В этом случае обнаружение раздела KVASCODE происходит намного быстрее, поскольку смещение в PAGE TABLE является фиксированным, что значительно сокращает процесс утечки до 1/512 попыток, а также облегчает этот процесс, просто выпуская содержимое только одного исполняемого файла PTE
NT утечка через Meltdown (выводы)
Утечка базового адреса NT - очень распространенная техника, используемая эксплойтами для повышения привилегий в ядре, где она используется для поиска SYSTEM и текущей структуры EPROCESS из связанного списка PsInitialSystemProcess, а затем для определения структуры TOKEN, которая используется для получения привилегии SYSTEM.
Получение базового адреса NT тривиально при работе на среднем уровне целостности, и его можно получить, просто вызвав функцию NtQuerySystemInformation. В процессах, работающих на низком уровне целостности, обычно в песочнице, необходимо иметь ошибку раскрытия информации, чтобы получить базовый адрес NT, как описано ранее. В большинстве случаев уязвимости раскрытия информации абсолютно необходимы для того, чтобы уйти от ограниченных процессов, таких как песочницы, используемые браузерами, такими как Chrome, Edge или Firefox.
Финальные заметки
Хотя Meltdown был действительно знаменитой атакой, и все операционные системы, работающие под управлением процессоров Intel, пострадали, полных доказательств того, что она используется в дикой среде нет. Хуже того, существует не так много общедоступных эксплойтов ядра, использующих Meltdown для эксплойтов Windows, за исключением некоторых POC. Этот пост является доказательством того, что эта атака по-прежнему полезна даже при включенной защите от Meltdown, и ее можно использовать в практических атаках для взлома Windows KASLR, которую затем можно использовать для выхода из любого изолированного приложения в сочетании с эксплоитом повышения привилегий ядра.
Источник: https://labs.bluefrostsecurity.de/blog/2020/06/30/meltdown-reloaded-breaking-windows-kaslr/
Автор перевода: yashechka
Переведено специально для портала xss.pro (c)
В начале 2018 года, была публично представлена одна из самых известных уязвимостей в истории: Meltdown (https://meltdownattack.com/). Хотя это была уязвимость центрального процессора, большинство операционных систем, включая Windows, добавили меры по защиты от этой атаки. Несмотря на то, что эти меры для Windows были эффективны для предотвращения Meltdown, они также привели к проблеме "дизайна", которой можно злоупотреблять даже сегодня.
Обратите внимание, что с Microsoft связались по этой проблеме, и их позиция заключается в том, что защита KVA Shadow не предназначена для защиты KASLR. Они специально указали на этот проектный документ ( https://msrc-blog.microsoft.com/2018/03/23/kva-shadow-mitigating-meltdown-on-windows/), и в частности на эту часть документа:
"Обратите внимание, что одним из следствий этого выбора конструкции является то, что Shadow KVA не защищает от атак на ASLR ядра с использованием спекулятивных побочных каналов. Это преднамеренное решение, учитывая сложность конструкции Shadow KVA, временные рамки и реалии других проблем с побочными каналами, влияющих на те же конструкции процессора. Примечательно, что процессоры, подверженные мошеннической загрузке на кэш данных, также обычно подвержены другим атакам на их BTB (целевые буферы ветвления) и другим микроархитектурным ресурсам, которые могут позволить раскрытие макета адресного пространства ядра локальному злоумышленнику, который выполняет произвольный собственный код".
В этом посте будет рассказано, как Meltdown все еще можно использовать для получения некоторых конкретных данных ядра и взлома Windows KASLR в последних версиях Windows, включая "Windows 10" 20H1.
Атака Meltdown
Атака Meltdown состояла в дампинге данных с произвольных адресов ядра, которые можно использовать для получения конфиденциальных данных, таких как (например, учетные данные), или использовать эксплоиты ядра, чтобы сделать эксплуатацию проще и надежнее.CVE, присвоенный Meltdown, был CVE-2017-5754 (https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2017-5754), который был представлен как уязвимость "Rogue Data Cache Load".
Meltdown была представлена одновременно со второй уязвимостью, обнаруженной теми же группами исследователей безопасности: Spectre. Кроме того, было обнаружено, что процессоры AMD не были затронуты Meltdown, но были затронуты Spectre.
Важно уточнить, что уязвимость Meltdown можно исправить с помощью обновления микрокода центрального процессора (не очень часто) или просто с помощью новейших моделей процессоров Intel на основе микроархитектуры "Cascade Lake" (смотри https://en.wikipedia.org/wiki/Cascade_Lake_(microarchitecture)).
Детали Meltdown - Часть 1
Эта атака возможна из-за состязания между доступом к памяти и проверкой привилегий в спекулятивном механизме выполнения, который позволяет считывать данные ядра из кода, работающего в пользовательском режиме. Обычно это невозможно из-за разрешений между памятью пользователя и ядра. Однако, Meltdown показала, что это все еще возможно, злоупотребляя побочными эффектами спекулятивных вычислений.
На очень высоком уровне, спекулятивное выполнение выполняется процессором, когда он не знает результатов какой-либо операции (например, доступ к памяти). Вместо того, чтобы ждать завершения операции, центральный процессор "спекулирует" результатами и продолжает выполнение на основе этой спекуляции. Когда результаты известны, предполагаемый код либо фиксируется (то есть его результаты становятся видимыми), либо просто отбрасывается.
В лучшем случае центральный процессор выполнил работу заранее, а в худшем - потратил немного вычислительной мощности вместо ожидания.
Прежде чем понять эту атаку, я (как и большинство людей) предположил, что эффекты спекулятивного выполнения были невидимы для выполняемого кода. Однако это оказалось далеко от истины ..
Чтобы понять, что мы подразумеваем под этим, давайте начнем с анализа примера, аналогичного примеру оригинальной статьи, но с использованием C вместо ассемблера:
C:
leaker_function ( unsigned __int64 pos )
{
if ( pos < array_size )
{
temp_value = array1 [ array2 [ pos ] ];
}
}
А теперь давайте предположим, что:
- pos полностью контролируется атакующим
- array_size равен 1
- array1 - массив без знака с 256 элементами
- array2 - массив без знака с 256 элементами
- temp_value это просто временная переменная для чтения array1
Из быстрого анализа единственный способ достичь условия что pos = 0, где позиция 0 массива array2 читается, а результат используется в качестве индекса для чтения array1. Выходные данные затем присваиваются значению temp_value.
Другими словами, любое значение pos больше 0 не достигнет кода внутри условия if.
Теперь давайте представим, что эта функция выполняется спекулятивно, где условие if предполагается истинным для произвольного значения pos:
C:
leaker_function ( unsigned __int64 pos )
{
if ( pos < array_size ) // 1, array_size = 1, but branch predictor assumes branch is taken
{
temp_value = array1 [ array2 [ pos ] ]; // if pos > 255, out-of-bounds access
}
}
В этом случае любая позиция array2 может быть прочитана, а затем загруженный байт будет использоваться как индекс для чтения одного элемента array1. Если значение pos слишком велико, его можно использовать для чтения памяти ядра относительно базового адреса array2, например:
array2 [ 0xffff8000'12345678 ]
Или лучше, прочитав один байт по произвольному адресу следующим образом:
array2 [ 0xffff8000'12345678 - &array2 ]
Проблема здесь в том, что этот код внутренне выполняется центральными процессором, а затем отбрасывается, потому что предположение было неверным. Таким образом, мы не можем видеть содержимое загруженного байта array2[pos], или мы можем?
Детали Meltdown - Часть 2
Вот где появляется вторая часть атаки Meltdown, в которой первая часть (уязвимость "Rogue Data Cache Load") сочетается с атакой в побочный канал кэша. Мы сказали, что невозможно увидеть результат array2[pos], но его содержимое сразу же используется как индекс в array1 [unknown]. Итак, хитрость заключается в том, чтобы определить, к какому элементу array1 мы обращались.
Хотя это может показаться на первый взгляд не очень интуитивным, но можно измерить время доступа для определенных областей памяти, таких как переменные или элементы массивов, из самого кода. Поскольку процессоры используют кеши для хранения содержимого недавно использованной памяти, а кеши значительно быстрее, чем обычная память, мы получаем возможность определить, к какому элементу обращались, наблюдая время доступа к различным строкам кеша.
Типичный способ определить, к какой строке кэша был произведен доступ, - сначала очистить все строки кэша, затем вызывать доступ к памяти жертвы, а затем синхронизировать доступ к каждой строке кэша. Линия, к которой имелся доступ жертвы, тогда произведет самое низкий значение тайминга. Этот метод обычно известен как Flush + Reload.
Мы можем очистить строки кэша для всех элементов массива измерений с помощью встроенной функции _mm_clflush:
C:
for ( i = 0 ; i < 256 ; i ++ )
{
// Invalidating cache
_mm_clflush ( &array1 [ i ] );
}
После этого, мы выполняем функцию, упомянутую выше (leaker_function), активируя спекулятивное выполнение. На последнем шаге, необходимо знать, к какому элементу массива обращались. Для этого, мы просто читаем каждый элемент array1 и измеряем время доступа, используя встроенную функцию __rdtscp.
Давайте рассмотрим пример, где pos = 0xffff8000'12345678 - &array2, что означает, что мы собираемся прочитать адрес ядра 0xffff8000'12345678:
C:
temp_value = array1 [ array2 [ 0xffff8000'12345678 - &array2 ] ];
И давайте предположим, что содержимое array2 [0xffff8000'12345678 - & array2] равно 0x41, поэтому происходит следующий доступ:
C:
temp_value = array1 [ 0x41 ];
Таким образом, когда код выполняется спекулятивно, доступ к позиции 0x41 массива1 будет осуществляться, и центральный процессора будет сохранять в кэше позицию 0x41 array1. При проверке элемента за элементом массива, время доступа для этой позиции должно быть меньше, чем для всех других позиций. Если это произойдет, мы можем предположить, что значение загруженного байта было 0x41, что означает, что содержимое адреса ядра 0xffff8000'12345678 равно 0x41.
Подводя итог, можно предпринять следующие шаги для реализации атаки Meltdown:
- Очистить кэш процессора для каждого элемента массива
- Выполнить функцию "leaker_function" так, чтобы во время выполнения доступ к памяти за пределами доступа осуществлялся спекулятивно
- Проверить время доступа для каждого элемента массива и получить лучший
Конечно, это ограниченное объяснение того, как на самом деле работает атака, и нужно учитывать и другие вещи, но этого должно быть достаточно для понимания общих принципов.
Детали Meltdown - Часть 3
К этому времени, вопрос состоит в том, как мы разрешаем спекулятивное исполнение? Один из способов сделать это - создать код, который выполняет некоторые "обфусцированные" вычисления, а затем поместить его в состояние цикла. Когда центральный процессор обнаруживает этот повторяющийся код, активируется выполнение "вне очереди", которое пытается оптимизировать выполнение путем нахождения всех возможных путей выполнения, допустимых или "вероятно" допустимых. В зависимости от выбранного пути выполнения центральный процессор может заранее прочитать память.
Когда поток выполнения наконец достигает условия чтения этой памяти, время доступа должно быть быстрее, потому что он был кэширован центральным процессором ранее. Используя в качестве вдохновения PoC, расположенный здесь (https://github.com/deeptechlabs/meltdown/blob/master/src/poc.c), мы можем увидеть способ запустить спекулятор и получить утечку памяти ядра, как показано ниже:
C:
// Kernel address to be read
malicious_pos = kernel_address - &array1;
// Looping 33 times
for ( i = 0 ; i <= 33 ; i ++ )
{
// Enabling speculator
pos = ( ( i % 33 ) - 1 ) & ~0xFFFF;
pos = ( pos | ( pos >> 16 ) );
pos = pos & malicious_pos;
// Leaking data when branch predictor is working
leaker_function ( pos );
}
Детали Meltdown - некоторые соображения
Очень важно сказать, что для возможности получения данных ядра из пользовательского режима с помощью этой атаки данные должны быть кэшированы центральным процессором, иначе нет никакой возможности что-либо получить. По этой причине, злоумышленник должен найти способ кеширования целевых данных, вызывая API ядра или просто вызывая исключения, но следя за тем, чтобы эти данные постоянно использовались процессором.
Еще одна важная вещь: процессор обычно использует строку кэша размером 64 байта (64-байтовый блок кэша), что означает, что, если мы сможем заставить процессор кэшировать содержимое адреса X ядра, будет загружена вся строка кэша, что даст нам возможность получить любой байта из этого 64-байтового диапазона, не ограничивая утечку одним байтом.
Страничные таблицы Intel
Когда AMD выпустила 64-разрядный процессор, основанный на наборе команд Intel x86, адресуемая виртуальная память была увеличена с 2^32 (4 гигабайта) до 2^48 (256 терабайт). Хотя центральный процессор работает в 64-битном режиме, только 48 бит используются для адресации виртуальной памяти (канонические адреса, где верхние 17 битов равны 0 или 1). Для этого процессор пропускает 16 бит виртуального диапазона адресов, разделяя виртуальную память на две части и помещая большую дыру в середину (неканонические адреса).
Диапазоны виртуальной памяти:
- 0x00000000'00000000 ~ 0x00007FFF'FFFFFFFF (canonical)
- 0x00008000'00000000 ~ 0xFFFF7FFF'FFFFFFFF (non canonical)
- 0xFFFF8000'00000000 ~ 0xFFFFFFFF'FFFFFFFF (canonical)
Чтобы отобразить 48 бит виртуальной памяти, процессор увеличил уровни подкачки с 2 до 4.
От самого высокого до самого низкого уровня имена таблиц подкачки:
- PML4
- PDPT (Page Directory Pointer Table)
- PD (Страница Каталогов)
- PT (Страница Таблиц)
На этих четырех уровнях подкачки каждая таблица имеет 512 записей (0x200), где каждая запись занимает 8 байтов. Таким образом, размер каждой таблицы составляет 0x200 * 8 = 0x1000 (4 КБ).
Нумерация от низшего к высшему уровню:
- Каждая запись таблицы страниц (PTE) может иметь адрес 4 КБ (0x1000 байт).
- Каждая запись каталога страниц (PDE) может иметь адрес 2 МБ (0x200 * 4KB = 0x200000 байт).
- Каждая запись в таблице указателей страниц (PDPTE) может адресовать 1 ГБ (0x200 * 2MB = 0x40000000 байт).
- Каждая запись PML4 может иметь адрес 512 ГБ (0x200 * 1 ГБ = 0x8000000000 байт).
Наконец, если мы умножим записи PML4 на 512 ГБ, мы сможем получить полностью адресуемую виртуальную память:
0x200 * 512GB = 0x10000'00000000 (256 terabytes)
На следующем рисунке показана 64-битная модель уровня подкачки:
Из обзора видно, что каждая запись на каждом уровне подкачки имеет одинаковый формат: она разделена на две части: разрешения и физический адрес (PFN), указанные в записи.
Среди наиболее важных битов защиты можно упомянуть:
- XD: eXecute Disable (бит 63) - обычно известный как бит NX (без выполнения)
- PS: Размер страницы (бит 7) - присутствует только в PDPTE и PDE
- U: Пользователь/супервизор (бит 2)
- R: Чтение/запись (бит 1)
- P: Присутствие (бит 0)
Где значение каждого бита следующее:
- XD: память не исполняется, когда она включена, в противном случае она исполняемая
- PS: это большая/огромная страница, в противном случае это обычная страница
- U: память доступна из режима пользователя, в противном случае это страница ядра (супервизор)
- R: память доступна только для чтения, в противном случае она также доступна для записи
- P: Память отображается, в противном случае она не отображается
Если бит PS включен, нижние уровни подкачки игнорируются, и в зависимости от уровня, на котором он включен, размер страницы может составлять 2 МБ или 1 ГБ.
Примечание. До появления 64-разрядных процессоров Intel для 32-разрядных процессоров использовались трехуровневые системы подкачки, позволяющие обрабатывать до 64 ГБ физической памяти с помощью поддержки PAE (расширение физических адресов), где таблица PDPT находилась на самом высоком уровне.
Патч Windows Meltdown
3 января 2018 года Microsoft выпустила исправление для уязвимости Meltdown (https://portal.msrc.microsoft.com/en-US/security-guidance/advisory/ADV180002 ). Оно было названо KVA Shadow (https://msrc-blog.microsoft.com/2018/03/23/kva-shadow-mitigating-meltdown-on-windows/), которое было основано на решении KPTI (Kernel Page Table Isolation) (https://en.wikipedia.org/wiki/Kernel_page-table_isolation), реализованном в Linux. По сути, это патч состоит в том, что большинство страниц памяти ядра отключается при выполнении в пользовательском режиме, чтобы уменьшить поверхность атаки. Таким образом, атака Meltdown перестает работать, в основном потому, что данные не утекают, даже если процессор уязвим.
Защита строиться на использовании двух разных таблиц подкачки: одна для выполнения в режиме ядра и одна для выполнения в пользовательском режиме. Таблица PML4, используемая для режима ядра, заполнена, поэтому память пользователя и ядра полностью отображается. Напротив, таблица PML4, используемая для пользовательского режима, представляет собой так называемый Shadow PML4, который отображает всю пользовательскую память и только небольшую часть памяти ядра.
На скриншотах ниже мы можем видеть полную PML4 и теневую PML4, где много записей отсутствуют.
Полная таблица PML4
Теневая таблица PML4
Переход между теневой и полной таблицами подкачки выполняется в режиме ядра, когда пользовательский процесс вызывает системный вызов или просто создает исключение (например, деление на ноль, недопустимая память и так далее).
Примечание: Код, работающий на высоком уровне целостности, например приложения, запускаемые с помощью параметра "Запуск от имени администратора", не использует теневую таблицу PML4.
Windows SMEP с помощью программного обеспечения
Аппаратная поддержка SMEP (Supervisor Mode Execution Prevention) была представлена в микроархитектуре Intel Ivy Bridge (https://en.wikipedia.org/wiki/Ivy_Bridge_(microarchitecture)). По сути, эта функция была добавлена, чтобы избежать выполнения кода пользовательского пространства в режиме ядра, что часто использовалось в прошлом для эксплоитов ядра. Эта функция присутствует на большинстве современных компьютеров в настоящее время.
Несмотря на эту аппаратную защиту и использование двух разных таблиц PML4 для реализации защиты от последствий Meltdown, Microsoft решила воспользоваться этим, добавив улучшение безопасности путем внедрения SMEP с помощью программного обеспечения.
На следующем рисунке мы видим записи пользователя из полной таблицы PML4:
Глядя на красные метки, мы видим, что пользовательские записи таблицы PML4 устанавливаются с битом XD (смотри https://en.wikipedia.org/wiki/Executable_space_protection), что означает, что весь пользовательский код НЕ исполняется в режиме ядра. Таким образом, компьютеры со старыми моделями центрального процессора должны быть защищены от эксплоитов ядра, как если бы присутствовала функция SMEP. В двух словах, любой эксплоит ядра, который пытается выполнить пользовательский код, потерпит крах.
Важно уточнить, что эта аппаратная защита SMEP может быть напрямую обойдена с использованием метода, описанного в презентации "Обход SMEP Windows: U = S" (https://www.coresecurity.com/core-labs/publications/windows-smep-bypass-us) или просто отключив эту функцию из регистра CR4 (20-й бит). С программной реализацией SMEP можно использовать те же методы, но необходимо добавить дополнительный шаг, отключив бит XD в записи таблицы PML4, связанной с целевым виртуальным адресом.
PML4 в Windows до рандомизации
Как упоминалось ранее, таблица PML4 является самым высоким уровнем подкачки в 64-битных моделях памяти. В Windows эта таблица используется для разделения памяти пользователя и ядра, просто используя самые низкие записи 0x100 (256) для отображения пользовательской памяти и самые высокие записи 0x100 для отображения памяти ядра. Зная, что каждая запись PML4 может отображать 512 ГБ (0x8000000000 байт) и для режима пользователя назначено 0x100 записей, мы можем вычесть диапазон виртуальных адресов пользователя, просто выполнив следующие вычисления:
user address range: 0 ~ 0x100 * 0x8000000000
Который так же, как:
user address range: 0 ~ 0x00007FFF'FFFFFFFF (0x8000'00000000 - 1)
Поскольку специальная техника, используемая Windows для управления пейджингом, называемая "self-referential", которая состоит из записи таблицы PML4, указывающей на себя (указывающей на таблицу PML4, которая содержит саму запись), виртуальные адреса для всей таблицы пейджинга могут быть рассчитывается просто зная адрес таблицы PML4. В то же время адрес PML4 можно рассчитать, просто зная, какая запись PML4 использовалась ядром Windows в качестве "self-referential" после загрузки.
Ниже приведен скриншот таблицы PML4, где можно увидеть ссылку на self-referential запись
Первоначально, эта таблица всегда размещалась по одному и тому же адресу ядра, потому что запись, которая раньше делала это, находилась в фиксированной позиции таблицы, расположенной в записи 0x1ED. Исходный адрес Windows таблицы PML4 можно рассчитать, выполнив следующие вычисления:
0xFFFF0000'00000000 + ( 0x8000000000 + 0x40000000 + 0x200000 + 0x1000 ) * 0x1ED = 0xFFFFF6FB7DBED000.
Адрес 0xFFFFF6FB7DBED000 использовался во всех версиях Windows, пока он не был рандомизирован.
PML4 в Windows после рандомизации
Рандомизация таблиц подкачки Windows была введена в "Windows 10" v1607 (RS1 - "Anniversary Update"), которая была выпущена в 2016 году. Так как он все еще использует технику self-referential, рандомизация была ограничена 256 позициями в таблице PML4, что означает, что таблица может быть распределена только по 256 различным адресам ядра, то есть действительно это плохая рандомизация. Как описано в предыдущем разделе, адрес таблицы PML4 можно рассчитать, зная, какая запись таблицы PML4 является self-referential.
Если мы хотим знать все возможные адреса PML4, мы можем сделать что-то вроде следующего:
C:
for ( entry = 0x100 ; entry < 0x200 ; entry ++ )
{
pml4_address = 0xFFFF000000000000 + ( 0x8000000000 + 0x40000000 + 0x200000 + 0x1000 ) * entry;
printf ( “PML4 address: %llx\n” , pml4_address );
}
Так как эта таблица была рандомизирована, и никакая Windows API не может сказать нам, где эта таблица расположена, злоупотребление таблицами подкачки эксплоитами ядра уменьшалось.
PML4 в реальных сценариях эксплуатации
Использование таблиц подкачки и злоупотребление ими при использовании эксплоитов ядра не очень популярны, скорее всего, из-за необходимости хорошего понимания системы подкачки. Действительно, использование содержимого памяти для отображения другой памяти не является тривиальной концепцией.
С другой стороны, модификации таблиц подкачки могут использоваться для создания примитивов, таких как произвольное чтение/запись ядра и даже выполнение кода ядра. Кроме того, они могут выполняться даже эксплоитами ядра, работающими на любом уровне целостности, включая Low IL. Это делает их очень мощным способом уйти от самых сложных реализаций песочницы, таких как процесс рендеринга Chrome.
Некоторые методы, основанные на злоупотреблении таблицами подкачки, перечислены ниже:
- Двойная запись: перезаписать пользовательский PDE и пользовательский PTE, указывая оба на один и тот же физический адрес, что дает нам контроль над целой PAGE TABLE из пользовательского режима, который можно использовать в качестве примитива чтения/записи!
- Огромная страница: перезаписать пользовательский PDPTE, включив бит "PS" и создав ОГРОМНУЮ СТРАНИЦУ, что дает нам возможность считывать/записывать 1 ГБ последовательной физической памяти, начиная с физического адреса, заданного в записи PDPT.
Важно уточнить, что эта функция "огромной страницы" уже некоторое время присутствует в процессорах, но мы все еще можем найти компьютеры, которые ее не поддерживают.
- Большая страница: перезаписать пользовательский PDE, включив бит "PS" и создав БОЛЬШУЮ СТРАНИЦУ, что дает нам возможность чтения/записи 2 МБ последовательной физической памяти, начиная с физического адреса, заданного в записи PDPT.
Хотя техника не такая мощная, как "Огромная страница", она может быть полезна в некоторых сценариях, когда у нас есть некоторая информация о содержимом физической памяти, которая должна отображаться, например, при создании большой страницы с использованием физического адреса NULL (номер PFN 0) и чтение или запись содержимого кучи HAL.
- Целевая страница: перезаписать пользовательский PTE, задав произвольный физический адрес. Это наименее распространенный сценарий, но это самый простой вариант, если вы точно знаете, по какому физическому адресу отображаются данные, которые вы хотите прочитать или записать.
- Самостоятельная ссылка: Создание self-referential entry на некоторых из четырех уровней подкачки. Если мы знаем физический адрес одной таблицы подкачки пользователя, мы могли бы перезаписать запись, указывающую на себя, которая обеспечивает примитив чтения/записи, манипулируя этой таблицей из пользовательского режима, подобно методике "двойной записи", описанной выше.
- Обход SMEP: перезаписать пользовательский PTE, отключив бит "U", преобразовав наш пользовательский код в код ядра.
В зависимости от метода, упомянутого выше, может потребоваться включить только несколько битов для создания правильной записи в таблице подкачки, что означает, что это действительно полезно для большинства условий write-what-where, возникающих в эксплойтах ядра.
PML4 экспозиция в Windows после исправления Meltdown
На этом этапе, необходимо сказать, что исправление Windows Meltdown имеет проблему "проектирования", когда не все данные, чувствительные к ядру, скрыты от пользовательского режима. Как упоминалось ранее, две разных таблицы PML4 используются для защиты от атаки Meltdown, которая используется процессором PML4 shadow при работе в пользовательском режиме.
И здесь возникает проблема: теневая таблица PML4 отображается в теневую память ядра, что означает, что она подвергается воздействию пользовательского режима даже при реализованном защите от Meltdown. Таким образом, и как следствие техники self-referential entry, все отображенные таблицы подкачки теневой таблицы PML4 могут быть получены с помощью атаки Meltdown!
Очень важно уточнить, что, хотя правильная настройка таблицы PML4 имеет решающее значение для работающей реализации виртуальной памяти, на самом деле нет необходимости отображать ее в виртуальной памяти или, по крайней мере, не отображать ее постоянно, а просто отображать, когда это необходимо. Ясно, что в некоторых случаях, когда пользовательская память отображается пользовательским кодом, ядру Windows приходится обновлять как теневую, так и всю PML4.
Это обновление выполняется в режиме ядра, где используется полная таблица PML4, а не теневая. Итак, поскольку теневая таблица PML4 представляет собой только блок памяти размером 4 КБ, эта таблица может отображаться в любой части пространства ядра, как и любое другое выделение памяти, без предоставления ее пользовательскому режиму.
Причина отображения таблиц подкачки в теневой таблицы PML4 не совсем ясна, за исключением проблем с производительностью между переключениями контекста между теневым и полным режимом и наоборот. На следующем снимке экрана мы можем видеть, как атака Meltdown способна сбросить первую часть таблицы PML4 в последней версии "Windows 10" (20H1), которая связана с диапазоном виртуальных адресов 0 ~ 0x380'00000000 (0 ~ 7GB):
Конечно, чтобы иметь возможность получить данных, показанных на рисунке выше, необходимо знать, где расположены таблицы подкачки.
Дерандомизация PML4 через Meltdown
В секции PML4 в Windows после рандомизации, описанном выше, мы видим, что рандомизация таблицы PML4 плохая, и она может принимать только 256 разных адресов. Кроме того, взглянув на предыдущий раздел, мы можем увидеть, что можно использовать утечку данных из таблиц подкачки с помощью атаки Meltdown.
Наконец, флаги разрешений, используемые self-referential entry, представлены значением 0x63 (Dirty, Accessed, Writable и Present), а его смещение в PML4 определяется адресом самого PML4. Таким образом, нахождение записи с этим байтом, установленным в 0x63 с правым смещением, с помощью Meltdown позволяет найти таблицу PML4. Собрав все это вместе, мы можем сделать вывод, что можно узнать, где расположена таблица PML4, просто выполнив следующий код:
C:
for ( entry = 0x100 ; entry < 0x200 ; entry ++ )
{
pml4_address = 0xFFFF000000000000 + ( 0x8000000000 + 0x40000000 + 0x200000 + 0x1000 ) * entry;
// Kernel address used to leak a single byte
if ( leak_byte_via_meltdown ( pml4_address + entry * 0x8 ) == 0x63 )
{
// PML4 found!
return ( pml4_address );
}
}
На следующем снимке экрана мы можем видеть, как адрес этой таблицы дерандомизируется с помощью Meltdown за 16 миллисекунд.
Код ликера PML4 можно скачать здесь (https://github.com/bluefrostsecurity/Meltdown-KVA-Shadow-Leak/).
Дерандомизация PML4 (выводы)
Вышеописанная дерандомизация обрекает усилия Microsoft по рандомизации таблиц подкачки, делая их снова предсказуемыми. Поскольку адрес PML4 не может быть получен путем вызова какого-либо API-интерфейса Windows, этот метод действительно полезен для любого эксплоита повышения привилегий ядра, работающего на низком или среднем уровне целостности.
Важно отметить, что до появления Meltdown еще одна техника дерандомизации PML4 была представлена на Ekoparty в лекции "Я знаю, где живет ваша страница: удаление рандомизации ядра Windows 10" (видео), которую дал Enrique Nissim (@kiqueNissim) в 2016 году.
NT утечка через Meltdown (вступление)
С появлением исправления Meltdown появилась группа новых функций NT. Эти функции были добавлены, чтобы иметь возможность обрабатывать системные вызовы и пользовательские исключения, когда эта защита включена. Все эти новые функции имеют имена, похожие на оригинальные, за исключением простого добавления к имени постфикса Shadow. Например, функция деления на нулевое исключение изначально называлась KiDivideErrorFault, а теперь — KiDivideErrorFaultShadow.
Эти функции Shadow являются просто обертками вокруг оригинальных, где выполняется переключение контекста с теневого на полные таблицы подкачки. На следующем снимке экрана мы видим, как загружаются полные таблицы подкачки при установке регистра CR3:
Весь этот код был помещен в файловый раздел с именем KVASCODE, который находится в модуле ntoskrnl.exe, где размер раздела составляет 0x3000 байт (3 страницы). Поскольку для поддержания работоспособности ОС должны присутствовать системные вызовы и исключения, необходимо сопоставить их с теневой стороной, что означает, что они должны быть доступны пользовательскому режиму. Если записи таблицы подкачки могут быть прочитаны с помощью Meltdown, имеет смысл подумать, что возможно утечка местоположения, где также отображается этот теневой код.
Утечка NT через Meltdown
Как объяснялось выше, 64-разрядная модель памяти Intel использует четыре уровня таблицы подкачки, где обычно для отображения виртуальной памяти используется самый низкий уровень (ТАБЛИЦА СТРАНИЦ). Чтобы определить местонахождение теневого кода, необходимо определить, какие PTE используются для сопоставления этого раздела кода. Поскольку это исполняемый раздел, соответствующие PTE отключили бит XD, что упрощает идентификацию теневого кода.
Таким образом, используя Meltdown, который дает нам возможность читать содержимое таблиц подкачки, можно найти этот код, обработав четыре уровня подкачки, начиная с теневого PML4 и переходя на следующий более низкий уровень, когда появляется действительная запись. Если этот процесс повторяется и обнаруживаются три последовательных исполняемых PTE, это означает, что был найден теневой код. Используя Meltdown, это можно сделать, выполнив следующие шаги:
C:
for ( pml4e = 0x100 ; pml4e < 0x200 ; pml4e ++ ) // Starting from 0x100 because user/kernel division
for ( pdpte = 0 ; pdpte < 0x200 ; pdpte ++ )
for ( pde = 0 ; pde < 0x200 ; pde ++ )
for ( pte = 0 ; pte < 0x200 ; pte ++ )
if ( is_three_consecutive_executable_PTEs ( pte ) == TRUE )
Shadow NT code found!
Как только этот код раздела найден, последним шагом является вычитание дельта-смещения из раздела KVASCODE в базовый адрес "ntoskrnl.exe", как показано ниже:
NT base = KVASCODE_address - KVASCODE_delta_from_NT
Поскольку этот теневой код является частью NT, получение базового адреса - это просто вычитание. Важно уточнить, что смещение (дельта) этого раздела изменяется между выпусками Windows, но обычно оно одинаково для разных версий ntoskrnl.exe одного и того же выпуска. В случае Windows 20H1 "Обновление от мая 2020 года" смещение от базового адреса NT составляет 0xa21000 байт, что соответствует страницам 0xa21.
На следующем снимке экрана мы можем видеть, как базовый адрес ntoskrnl.exe может быть получен за 900 миллисекунд:
Ликер NT можно скачать здесь. (https://github.com/bluefrostsecurity/Meltdown-KVA-Shadow-Leak/)
Примечание. Начиная с Windows 10 RS6, при работе на компьютерах с оперативной памятью, равной или превышающей 4 ГБ, базовый адрес ntoskrnl.exe выравнивается до 2 МБ.
В этом случае обнаружение раздела KVASCODE происходит намного быстрее, поскольку смещение в PAGE TABLE является фиксированным, что значительно сокращает процесс утечки до 1/512 попыток, а также облегчает этот процесс, просто выпуская содержимое только одного исполняемого файла PTE
NT утечка через Meltdown (выводы)
Утечка базового адреса NT - очень распространенная техника, используемая эксплойтами для повышения привилегий в ядре, где она используется для поиска SYSTEM и текущей структуры EPROCESS из связанного списка PsInitialSystemProcess, а затем для определения структуры TOKEN, которая используется для получения привилегии SYSTEM.
Получение базового адреса NT тривиально при работе на среднем уровне целостности, и его можно получить, просто вызвав функцию NtQuerySystemInformation. В процессах, работающих на низком уровне целостности, обычно в песочнице, необходимо иметь ошибку раскрытия информации, чтобы получить базовый адрес NT, как описано ранее. В большинстве случаев уязвимости раскрытия информации абсолютно необходимы для того, чтобы уйти от ограниченных процессов, таких как песочницы, используемые браузерами, такими как Chrome, Edge или Firefox.
Финальные заметки
Хотя Meltdown был действительно знаменитой атакой, и все операционные системы, работающие под управлением процессоров Intel, пострадали, полных доказательств того, что она используется в дикой среде нет. Хуже того, существует не так много общедоступных эксплойтов ядра, использующих Meltdown для эксплойтов Windows, за исключением некоторых POC. Этот пост является доказательством того, что эта атака по-прежнему полезна даже при включенной защите от Meltdown, и ее можно использовать в практических атаках для взлома Windows KASLR, которую затем можно использовать для выхода из любого изолированного приложения в сочетании с эксплоитом повышения привилегий ядра.
Источник: https://labs.bluefrostsecurity.de/blog/2020/06/30/meltdown-reloaded-breaking-windows-kaslr/
Автор перевода: yashechka
Переведено специально для портала xss.pro (c)