Год с Spectre: взгляд команды V8
3 января 2018 года Google Project Zero и другие раскрыли первые три уязвимости нового класса, которые затрагивают процессоры с механизмом спекулятивного исполнения, названные Spectre и Meltdown. Используя спекулятивное исполнение процессоров, злоумышленник мог временно обходить как явные, так и неявные проверки безопасности в коде, которые предотвращают чтение несанкционированных данных в памяти. Хотя процессорная спекуляция была задумана как микроархитектурная деталь, невидимая на уровне архитектуры, специально разработанные программы могли читать несанкционированную информацию во время спекуляции и раскрывать её через побочные каналы, такие как время выполнения фрагмента программы.
Когда было показано, что JavaScript может быть использован для осуществления атак Spectre, команда V8 включилась в решение проблемы. Мы сформировали команду экстренного реагирования и тесно сотрудничали с другими командами в Google, нашими партнёрами из других разработчиков браузеров и нашими партнёрами из сферы аппаратного обеспечения. Совместно с ними мы активно занимались как наступательными исследованиями (создавали прототипы гаджетов), так и защитными исследованиями (разрабатывали меры предотвращения потенциальных атак).
Атака Spectre состоит из двух частей:
- Утечка недоступных данных в скрытое состояние процессора. Все известные атаки Spectre используют спекуляцию для утечки битов недоступных данных в кэш процессора.
- Извлечение скрытого состояния для восстановления недоступных данных. Для этого злоумышленнику требуется таймер с достаточной точностью. (Удивительно, но даже низкорезолюционные таймеры могут быть достаточно точными, особенно с использованием таких техник, как граничный порог.)
Теоретически было бы достаточно устранить любую из двух частей атаки. Поскольку мы не знаем способа идеально устранить какую-либо из частей, мы разработали и внедрили меры защиты, которые значительно сокращают объём информации, которая утечёт в кэш процессора, а также меры, которые затрудняют восстановление скрытого состояния.
Высокоточные таймеры
Малейшие изменения состояния, которые могут остаться после спекулятивного исполнения, порождают соответствующие tiny малейшие разницы в времени — порядка миллиардной доли секунды. Чтобы напрямую обнаружить такие разницы, вредоносная программа требует высокоточного таймера. Процессоры предоставляют такие таймеры, но сам веб-платформа их не предоставляет. Самый точный таймер веб-платформы, performance.now()
, имел разрешение в несколько микросекунд, что первоначально считалось недоступным для этой цели. Однако два года назад академическая исследовательская группа, специализирующаяся на микроархитектурных атаках, опубликовала статью, в которой изучалась доступность таймеров в веб-платформе. Они заключили, что совместное изменяемое разделяемое память и различные техники восстановления разрешения могут позволить создание таймеров ещё более высокого разрешения, вплоть до наносекунд. Такие таймеры достаточно точны, чтобы обнаружить отдельные попадания и промахи кэша L1, что обычно используется как способ утечки информации гаджетами Spectre.
Меры по снижению точности таймеров
Чтобы нарушить возможность определения мелких временных различий, разработчики браузеров применили комплексный подход. Во всех браузерах разрешение performance.now()
было уменьшено (в Chrome — с 5 микросекунд до 100), а случайный равномерный джиттер был введён для предотвращения восстановления разрешения. После консультаций между всеми разработчиками мы совместно решили предпринять беспрецедентный шаг — немедленно и ретроспективно отключить API SharedArrayBuffer
во всех браузерах, чтобы предотвратить создание наносекундного таймера, который мог бы быть использован для атак Spectre.
Усиление
На раннем этапе наших наступательных исследований стало ясно, что меры по снижению точности таймеров сами по себе не будут достаточны. Одной из причин этого является то, что злоумышленник может просто многократно выполнять свой гаджет, так чтобы совокупное различие во времени было значительно больше, чем одиночное попадание или промах кэша. Нам удалось разработать надёжные гаджеты, которые используют множество линий кэша одновременно, до предела ёмкости кэша, что давало различия во времени до 600 микросекунд. Позже мы обнаружили произвольные техники усиления, которые не ограничены ёмкостью кэша. Такие техники усиления основываются на многократных попытках считать секретные данные.
Меры для JIT
Чтобы считать недоступные данные с помощью Spectre, злоумышленник заставляет процессор спекулятивно выполнять код, который читает обычно недоступные данные и кодирует их в кэш. Атака может быть предотвращена двумя способами:
- Предотвратить спекулятивное выполнение кода.
- Предотвратить спекулятивное выполнение, при котором считываются недоступные данные.
Мы пробовали метод (1), вставляя рекомендованные инструкции для барьеров спекуляции, такие как LFENCE
от Intel, на каждую критическую ветвь условного оператора и применяя retpolines для косвенных ветвей. К сожалению, такие радикальные меры значительно снижают производительность (замедление в 2–3 раза на тесте Octane). Вместо этого мы выбрали подход (2), добавляя последовательности кода, которые предотвращают считывание секретных данных из-за ошибочных спекуляций. Позвольте нам продемонстрировать технику на следующем фрагменте кода:
if (condition) {
return a[i];
}
Для простоты предположим, что condition принимает значения 0
или 1
. Уязвимость возникает, если процессор спекулятивно считывает значение из a[i]
, когда i
находится вне допустимых границ, получая доступ к недоступным данным. Важно заметить, что в таком случае спекуляция пытается считать a[i]
, когда condition
равно 0
. Наша методика переписывает эту программу так, чтобы она работала точно так же, как оригинальная, но не утекали спекулятивно загруженные данные.
Мы резервируем один регистр процессора, который называем «ядовитым» (poison), чтобы отслеживать, исполняется ли код в ветке с ошибочным предсказанием. Регистр poison поддерживается через все ветви и вызовы в сгенерированном коде, так что любая ветка с ошибочным предсказанием приводит к тому, что регистр poison становится равным 0
. Затем мы инструментируем все обращения к памяти так, чтобы они безусловно маскировали результат всех загрузок текущим значением регистра poison. Это не предотвращает процессор от предсказания (или ошибочного предсказания) ветвей, но уничтожает информацию о (потенциально выходящих за границы) загруженных значениях из-за ошибочных предсказаний ветвей. Инструментированный код приведен ниже (предполагается, что a
— массив чисел).
let poison = 1;
// …
if (condition) {
poison *= condition;
return a[i] * poison;
}
Дополнительный код не оказывает никакого влияния на нормальное (определенное архитектурой) поведение программы. Он влияет только на состояние микроархитектуры при работе на процессорах с спекулятивным выполнением. Если программа была инструментирована на уровне исходного кода, современные компиляторы могут удалить такую инструментализацию благодаря продвинутым оптимизациям. В V8 мы предотвращаем удаление таких мер компилятором, добавляя их на очень позднем этапе компиляции.
Мы также используем технику «ядовитости», чтобы предотвратить утечки из-за ошибочных предположений косвенных ветвей в цикле обработки байт-кода интерпретатора и последовательности вызовов функций JavaScript. В интерпретаторе мы устанавливаем poison в 0
, если обработчик байт-кода (т.е. машинный код для интерпретации одного байт-кода) не совпадает с текущим байт-кодом. Для вызовов JavaScript мы передаем целевую функцию как параметр (в регистре) и устанавливаем poison в 0
в начале каждой функции, если входящая целевая функция не совпадает с текущей функцией. С применением этих мер мы наблюдаем менее 20% снижения производительности на тесте Octane.
Меры безопасности для WebAssembly проще, так как главным проверяемым моментом является гарантия, что доступ к памяти находится в пределах границ. Для 32-битных платформ, помимо обычных проверок границ, мы добавляем память до следующей степени двойки и безусловно маскируем любые верхние биты предоставленного пользователем индекса памяти. 64-битные платформы не требуют таких мер, так как реализация использует защиту виртуальной памяти для проверки границ. Мы пробовали компилировать операторы switch/case в код бинарного поиска вместо использования потенциально уязвимой косвенной ветки, но это оказалось слишком дорого для некоторых нагрузок. Косвенные вызовы защищаются с помощью ретполайнов.
Программные меры безопасности — это неустойчивый путь
К счастью или к сожалению, наши исследования в области наступательных методов продвинулись намного быстрее, чем исследования по защите, и мы быстро обнаружили, что программное смягчение всех возможных утечек из-за Spectre является неосуществимым. Это объясняется рядом причин. Во-первых, усилия инженеров, направленные на борьбу со Spectre, не соответствовали уровню угрозы. В V8 мы сталкиваемся с множеством других угроз безопасности, которые намного хуже, например из-за ошибок прямого выхода за пределы памяти (быстрее и прямее, чем Spectre), записи за пределами памяти (невозможно с Spectre, и хуже) и возможного удаленного выполнения кода (невозможно с Spectre, и намного, намного хуже). Во-вторых, созданные и реализованные нами всё более сложные механизмы смягчения приводили к значительной сложности, что является техническим долгом и может фактически увеличивать поверхность атаки, а также вызывать накладные расходы по производительности. В-третьих, тестирование и поддержка механизмов смягчения утечек микроархитектуры ещё сложнее, чем сами проектирование гаджетов, так как трудно быть уверенным, что механизмы работают так, как задумано. По крайней мере один раз важные меры смягчения были фактически отменены поздними оптимизациями компиляции. В-четвёртых, мы обнаружили, что эффективное смягчение некоторых вариантов Spectre, в частности варианта 4, просто невозможно на программном уровне, даже после героических усилий наших партнеров из Apple по борьбе с проблемой в их JIT-компиляторе.
Изоляция сайтов
Наше исследование пришло к выводу, что, в принципе, ненадежный код может читать всю адресную область процессов, используя Spectre и побочные каналы. Программные меры смягчения снижают эффективность многих возможных гаджетов, но не являются эффективными или всеобъемлющими. Единственным эффективным смягчающим фактором является перемещение чувствительных данных из адресного пространства процесса. К счастью, в Chrome уже много лет велись работы по разделению сайтов на разные процессы, чтобы уменьшить поверхность атаки из-за обычных уязвимостей. Эти инвестиции оправдали себя, и мы внедрили и развернули изоляцию сайтов на максимальном количестве платформ к маю 2018 года. Таким образом, модель безопасности Chrome больше не основывается на конфиденциальности, обеспечиваемой языком программирования, внутри процесса renderer.
Spectre стал долгим путешествием и показал лучшее в сотрудничестве между поставщиками из индустрии и академической среды. Пока что белые хакеры опережают черных. У нас до сих пор нет информации о реальных атаках, за исключением любопытных экспериментаторов и профессиональных исследователей, разрабатывающих концептуальные гаджеты. Новые варианты этих уязвимостей продолжают появляться и могут продолжать это ещё какое-то время. Мы продолжаем отслеживать эти угрозы и относимся к ним всерьёз.
Как и многие разработчики с опытом в области языков программирования и их реализации, идея того, что безопасные языки обеспечивают правильную границу абстракции, не позволяя корректным программам читать произвольную память, была гарантией, на которой строились наши ментальные модели. Удручающий вывод заключается в том, что наши модели были ошибочны — эта гарантия неверна на сегодняшнем оборудовании. Конечно, мы всё ещё считаем, что безопасные языки имеют большие инженерные преимущества и будут оставаться основой для будущего, но… на сегодняшнем оборудовании они немного текут.
Заинтересовавшиеся читатели могут углубиться в детали, прочитав наш аналитический документ.