V8: один год со Spectre +26


3 января 2018 года Google Project Zero и другие раскрыли первые три из нового класса уязвимостей, которые затрагивают процессоры со спекулятивным выполнением. Их назвали Spectre (1 и 2) и Meltdown. Используя механизмы спекулятивного выполнения CPU, злоумышленник может временно обойти как явные, так и неявные программные проверки безопасности, которые не позволяют программам читать недоступные данные в памяти. В то время как спекулятивное выполнение разработано как деталь микроархитектуры, невидимая на архитектурном уровне, тщательно разработанные программы могли считывать недоступную информацию в спекулятивном блоке и раскрывать её через побочные каналы, такие как время выполнения фрагмента программы.

Когда было показано, что атаки Spectre возможны средствами JavaScript, команда V8 приняла участие в решении проблемы. Мы сформировали группу реагирования на чрезвычайные ситуации и тесно сотрудничали с другими командами в Google, нашими партнёрами из числа разработчиков других браузеров и партнёрами по оборудованию. Совместно с ними мы проактивно вели как наступательные исследования (конструирование атакующих модулей для доказательства концепции), так и оборонительные (смягчение потенциальных атак).

Атака Spectre состоит из двух частей:

  • Утечка недоступных в противном случае данных в скрытое состояние CPU. Все известные атаки Spectre используют спекуляции для передачи битов недоступных данных в кэши CPU.
  • Извлечение скрытого состояния, чтобы восстановить недоступные данные. Для этого злоумышленнику нужны часы достаточной точности. (На удивление невысокой точности, особенно с такими методами, как edge thresholding — сравнение с порогом вдоль выделяемого контура).

Теоретически, достаточно было бы заблокировать любой из двух компонентов атаки. Поскольку мы не знаем, как полностью заблокировать ни один из них, мы разработали и развернули смягчения, которые значительно уменьшают объём информации, просачивающейся в кэши CPU, и смягчения, которые затрудняют восстановление скрытого состояния.

Высокоточные таймеры


Крошечные изменения состояния, которые остаются после спекулятивного выполнения, порождают соответственно крошечные, почти невозможно крошечные, временны?е различия — порядка миллиардной доли секунды. Для непосредственного обнаружения отдельных таких различий атакующей программе требуется высокоточный таймер. Процессоры предлагают такие таймеры, но веб-платформа их не выставляет. У самого точного таймера на веб-платформе performance.now() было разрешение в несколько микросекунд, которое изначально считалось непригодным для этой цели. Однако два года назад научно-исследовательская группа, специализирующаяся на микроархитектурных атаках, опубликовала статью о таймерах на веб-платформе. Они пришли к выводу, что одновременная изменяемая общая память и различные методы восстановления разрешения позволяют создать таймеры ещё более высокого разрешения, вплоть до наносекундного. Такие таймеры достаточно точны, чтобы обнаружить отдельные хиты и промахи кэша L1. Именно он обычно используется для съёма информации в атаках Spectre.

Защита для таймера


Чтобы нарушить способность обнаруживать небольшие различия во времени, разработчики браузеров выбрали многосторонний подход. Во всех браузерах было уменьшено разрешение performance.now() (в Chrome с 5 микросекунд до 100) и введён случайный джиттер, чтобы предотвратить восстановление разрешения. После консультаций между разработчиками всех браузеров мы вместе решили предпринять беспрецедентный шаг: немедленного и ретроактивно отключить SharedArrayBuffer API во всех браузерах, чтобы предотвратить создание наносекундного таймера.

Усиление


В начале наших наступательных исследований стало ясно, что одних только смягчений таймера недостаточно. Одна из причин заключается в том, что злоумышленник может просто многократно запускать свой код, чтобы кумулятивная разница во времени была намного больше, чем одно попадание или промах кэша. Мы смогли сконструировать надёжные «гаджеты», которые используют много строк кэша за раз, вплоть до всей ёмкости кэша, что даёт разницу во времени до 600 микросекунд. Позже мы обнаружили произвольные методы усиления, которые не ограничены ёмкостью кэша. Такие методы усиления основаны на многократных попытках считывания секретных данных.

Защита JIT


Чтобы прочитать недоступные данные с помощью Spectre, злоумышленник заставляет CPU спекулятивно выполнять код, который считывает обычно недоступные данные и помещает их в кэш. Защиту можно рассмотреть с двух сторон:

  1. Предотвращение спекулятивного выполнения кода.
  2. Предотвращение чтения недоступных данных со стороны спекулятивного конвейера.

Мы экспериментировали с первым вариантом, вставляя рекомендуемые инструкции для предотвращения спекуляций, такие как LFENCE от Intel, на каждую критическую условную ветвь и используя ретполины для косвенных ветвей. К сожалению, такие тяжёлые смягчения значительно снижают производительность (замедление в 2-3 раза на бенчмарке Octane). Вместо этого мы выбрали второй подход, вставив последовательности смягчения, которые предотвращают чтение секретных данных из-за неправильных спекуляций. Позвольте проиллюстрировать технику следующим фрагментом кода:

if (condition) {
  return a[i];
}

Для простоты предположим, что условие 0 или 1. Приведённый выше код уязвим, если CPU спекулятивно считывает из a[i], когда i находится вне пределов, получая доступ к обычно недоступным данным. Важным наблюдением является то, что в таком случае спекуляция пытается прочитать a[i], когда условие равно 0. Наше смягчение переписывает эту программу так, что она ведёт себя точно так же, как оригинальная программа, но не допускает утечки каких-либо спекулятивно загруженных данных.

Мы резервируем один регистр CPU, который мы называем «ядом» (poison), чтобы отслеживать, выполняется ли код в неправильно интерпретируемой ветви. Ядовитый регистр поддерживается во всех ветвях и вызовах сгенерированного кода, так что любая неправильно интерпретированная ветвь заставляет ядовитый регистр становиться 0. Затем мы измеряем все обращения к памяти так, чтобы они безоговорочно маскировали результат всех загрузок текущим значением регистра poison. Это не мешает процессору предсказывать (или неверно интерпретировать) ветви, но уничтожает информацию (потенциально вне пределов) загруженных значений из-за неверно интерпретированных ветвей. Инструментальный код показан ниже (a — это массив чисел).

let poison = 1;
// …
if (condition) {
  poison *= condition;
  return a[i] * poison;
}

Дополнительный код не оказывает никакого влияния на нормальное (определённое архитектурой) поведение программы. Он влияет только на микро-архитектурное состояние при работе на CPU со спекулятивным выполнением. Если инструментировать программу на уровне исходного кода, расширенные оптимизации в современных компиляторах могут удалить такое инструментирование. В V8 мы предотвращаем удаление компилятором смягчений, вставляя их на очень поздней стадии компиляции.

Мы также используем эту технику «отравления» для предотвращения утечек из непрямых ветвей в цикле отправки байт-кода интерпретатора и в последовательности вызовов функций JavaScript. В интерпретаторе мы устанавливаем яд в 0, если обработчик байт-кода (т. е. последовательность машинного кода, которая интерпретирует один байт-код) не соответствует текущему байт-коду. Для вызовов JavaScript мы передаём целевую функцию в качестве параметра (в регистре) и устанавливаем яд в 0 в начале каждой функции, если входящая целевая функция не соответствует текущей функции. При таком смягчении мы видим замедление менее чем на 20% по бенчмарку Octane.

Смягчение для WebAssembly проще, так как основная проверка безопасности заключается в обеспечении доступа к памяти в пределах границ. Для 32-разрядных платформ, в дополнение к обычным проверкам границ, мы заполняем всю память до следующей степени двух и безоговорочно маскируем любые верхние биты пользовательского индекса памяти. 64-разрядные платформы не нуждаются в таком смягчении, так как реализация использует защиту виртуальной памяти для проверок границ. Мы экспериментировали с компиляцией операторов switch/case в двоичный поисковый код вместо использования потенциально уязвимой косвенной ветви, но это слишком дорого для некоторых рабочих нагрузок. Косвенные вызовы защищены ретполинами.

Защита программного обеспечения — ненадёжный вариант


К счастью или к несчастью, наши наступательные исследования продвигались намного быстрее, чем оборонительные, и мы быстро обнаружили невозможность программного смягчения всех возможных утечек в ходе атак Spectre. Это обусловлено рядом причин. Во-первых, инженерные усилия, направленные на борьбу со Spectre, несоразмерны уровню угрозы. В V8 мы сталкиваемся со многими другими угрозами безопасности, которые намного хуже, от прямого чтения за пределами границ из-за обычных багов (что быстрее и проще, чем Spectre), записи за пределы границ (это невозможно со Spectre и хуже) и потенциального удалённого выполнения кода (невозможно со Spectre и намного, намного хуже). Во-вторых, всё более сложные меры по смягчению последствий, которые мы разработали и внедрили, несли в себе значительную сложность, которая является техническим долгом и может фактически увеличить поверхность атаки и накладные расходы на производительность. В-третьих, тестирование и поддержание смягчения микроархитектурных утечек ещё сложнее, чем проектирование самих гаджетов для атаки, поскольку трудно быть уверенным, что смягчения продолжают работать так, как они были разработаны. По крайней мере один раз важные смягчения были эффективно отменены более поздними оптимизациями компилятора. В-четвёртых, мы обнаружили, что эффективное смягчение некоторых вариантов Spectre, особенно варианта 4, просто невозможно в программном обеспечении, даже после героических усилий наших партнёров в Apple по борьбе с проблемой в их JIT-компиляторе.

Изоляция сайтов


Наши исследования привели к выводу: в принципе, ненадёжный код может считать всё адресное пространство процесса с помощью Spectre и побочных каналов. Программные смягчения снижают эффективность многих потенциальных гаджетов, но не являются эффективными или всеобъемлющими. Единственной эффективной мерой является перемещение конфиденциальных данных за пределы адресного пространства процесса. К счастью, Chrome уже много лет пытается разделить сайты по разным процессам, чтобы уменьшить поверхность атаки из-за обычных уязвимостей. Эти инвестиции окупились, и к маю 2018 года мы довели до стадии готовности и развернули изоляцию сайтов на максимальном количестве платформ. Таким образом, модель безопасности Chrome больше не предполагает языковой конфиденциальности в процессе рендерера.

Spectre прошёл долгий путь и подчеркнул достоинства сотрудничества разработчиков в индустрии и научных кругах. Пока что белые шляпы опережают чёрных. Мы до сих пор не знаем ни об одной реальной атаке, за исключением любопытных экспериментаторов и профессиональных исследователей, разрабатывающих гаджеты для доказательства концепции. Новые варианты этих уязвимостей продолжают появляться и это продолжится в течение некоторого времени. Мы продолжаем отслеживать эти угрозы и серьёзно к ним относиться.

Как и многие программисты, мы тоже думали, что безопасные языки обеспечивают правильную границу абстракции, не позволяя хорошо типизированным программам читать произвольную память. Печально, что это оказалось ошибкой — эта гарантия не соответствует сегодняшнему оборудованию. Конечно, мы по-прежнему считаем, что у безопасных языков больше инженерных преимуществ, и за ними будущее, но… на сегодняшнем оборудовании они немного протекают.

Заинтересованные читатели могут углубиться в тему и получить более подробную информацию в нашей научной статье.




К сожалению, не доступен сервер mySQL