Spectre — группа аппаратных уязвимостей, ошибка в большинстве современных процессоров, имеющих спекулятивное выполнение команд и развитое предсказание ветвлений, позволяющих проводить чтение данных через сторонний канал в виде общей иерархии кэш-памяти. Затрагивает большинство современных микропроцессоров, в частности, архитектур х86/x86_64 (Intel и AMD) и некоторые процессорные ядра ARM[1].
Уязвимость потенциально позволяет локальным приложениям (локальному атакующему, при запуске специальной программы) получить доступ к содержимому виртуальной памяти текущего приложения или других программ[2][3][4]. Угрозе присвоены два CVE-идентификатора: CVE-2017-5753 и CVE-2017-5715.
Spectre была обнаружена независимо исследователями североамериканской корпорации Google (проект Zero) и группой, сотрудничающей с Paul Kocher, при участии сотрудников Грацского технического университета. Уязвимость была найдена в середине 2017 года и несколько месяцев находилась на стадии закрытого обсуждения и исправления. Публикация подробной информации и исправлений была запланирована на 9 января 2018 года, но детали уязвимости были обнародованы 4 января 2018 года одновременно с атакой Meltdown, из-за публикаций журналистов The Register[5], которые узнали об исправлениях KAISER/KPTI для борьбы с Meltdown из списка рассылки ядра Linux[6].
Ошибка Spectre позволяет злонамеренным пользовательским приложениям, работающим на данном компьютере, получить доступ на чтение к произвольным местам компьютерной памяти, используемой процессом-жертвой, например другими приложениями (то есть нарушить изоляцию памяти между программами). Атаке Spectre подвержено большинство компьютерных систем, использующих высокопроизводительные микропроцессоры, в том числе персональные компьютеры, серверы, ноутбуки и ряд мобильных устройств[7]. В частности, атака Spectre была продемонстрирована на процессорах производства корпораций Intel, AMD и на чипах, использующих процессорные ядра ARM.
Имеется вариант атаки Spectre, использующий JavaScript-программы для получения доступа к памяти браузеров (чтение данных других сайтов или данных, сохраненных в браузере)[8].
Предположим, что фрагмент кода процесса-жертвы
if (x < array1_size)
y = array2[array1[x] * 256];
является частью функции, получающей беззнаковое целое x из ненадежного источника, а процесс, выполняющий этот код, имеет доступ к массиву беззнаковых 8-битных целых array1 размером array1_size, и ко второму массиву беззнаковых 8-битных целых array2 размером 64 кб.
Данный фрагмент начинается с проверки того, что значение x является допустимым. И эта проверка является существенной с точки зрения безопасности. В частности, она предотвращает чтение информации за пределами границ массива array1. В случае её отсутствия недопустимые значения x могут привести или к возникновению исключения, при попытке чтения данных за пределами доступной процессу памяти, или к чтению доступной процессу конфиденциальной информации путем задания x = <адрес_секретного_байта> - <адрес_массива_array1>.
К несчастью, неверное предсказание условного перехода при спекулятивном исполнении команд может привести к выполнению той ветви программного кода, которая в нормальных условиях никогда бы не выполнилась[9].
Например, приведенный выше фрагмент кода может быть выполнен в следующих условиях:
Такие условия могут возникать спонтанно, однако, могут быть и сформированы целенаправленно, например, путем чтения большого объёма посторонних данных, с целью заполнения этими данными кэша процессора и, соответственно, выбивания array1_size и array2 из кэша, а затем вызова функции ядра, задействующей секретный байт k, с целью помещения его в кэш. Впрочем, если структура кэша известна или же процессор добровольно предоставляет инструкцию обнуления кэша (например, инструкция cflush процессоров семейства x86), то задача по созданию необходимых условий для выполнения фрагмента кода существенно упрощается.
Выполнение фрагмента кода начинается со сравнения значения x со значением array1_size. Чтение значения array1_size в описанных выше условиях приводит к промаху кэша, что в свою очередь приведёт к ожиданию, когда значение array1_size будет получено из оперативной памяти. Из-за наличия в процессоре механизма спекулятивного исполнения команд, в течение времени ожидания процессор не будет простаивать, а попытается выполнить одну из ветвей программного кода, следующего за инструкцией ветвления.
Так как предыдущие обращения к фрагменту выполнялись с допустимыми значениями x, то предсказатель ветвлений предположит, что и в этот раз предикат (x < array1_size) окажется истинным, и процессор попытается выполнить соответствующую последовательность инструкций. А именно, он прочитает байт по адресу <адрес_массива_array1> + x, то есть секретный байт k, который, благодаря сформированным специально условиям, уже находится в кэше. Затем, процессор использует полученное значение для вычисления выражения k * 256 и чтения элемента массива array2[k * 256], которое приведет ко второму промаху кэша, и ожиданию получения значения array2[k * 256] из оперативной памяти. В это время из оперативной памяти будет получено значение array1_size, процессор распознает ошибку предсказателя ветвлений, и восстановит архитектурное состояние на момент до начала выполнения неверной ветви программного кода.
Однако, на реальных процессорах спекулятивное чтение array2[k * 256] повлияет на состояние кэша процессора, и это состояние будет зависеть от k. Для завершения атаки требуется лишь выявить это изменение при помощи атаки по сторонним каналам (атакующий должен иметь доступ к общему процессорному кэшу и источнику точного времени), и, основываясь на нём, вычислить секретный байт k. Это легко осуществить, так как чтения элементов массива array2[n * 256] будет выполняться быстро для n = k и медленно — для остальных значений.
Косвенный переход может использовать для ветвления больше, чем два адреса. Например, инструкции процессоров семейства x86 могут выполнять переход, используя значение адреса в регистре (jmp eax), в памяти (jmp [eax], или jmp dword ptr [0xdeadc0de]), или в стеке (ret). Инструкции косвенных переходов имеются также в процессорах ARM (mov pc, r14), MIPS (jr $ra), SPARC (jmpl %o7), RISC-V (jalr x0,x1,0), и многих других.
Если определение адреса косвенного перехода откладывается из-за промаха кэша, и предсказатель косвенных переходов «натренирован» с использованием специально подобранных адресов, может произойти спекулятивное исполнение команд по адресу, заданному злоумышленником. Команд, которые иначе никогда бы не были выполнены. Если такое исполнение оставляет измеримые побочные эффекты, то его использование становится мощным инструментом в руках атакующего.
В настоящее время не существует готовых программных технологий защиты от атаки Spectre, хотя ведется определённая работа[10]. По данным веб-сайта, посвященному продвижению атаки, «Это не так легко исправить, и она (ошибка) будет преследовать нас в течение длительного времени».
Программное исправление может включать в себя перекомпиляцию ПО при помощи новых компиляторов с заменой уязвимых последовательностей машинного кода (т. н. механизм «retpoline», реализован в GCC и Clang/LLVM)[11].
Производителями процессоров предложено несколько вариантов исправлений, некоторые из которых требуют обновлений микрокода процессора, другие — добавления новых инструкций в будущие процессоры. Исправления должны сочетаться с перекомпиляцией ПО[11].
В ранних версиях уведомления CVE по Spectre организация CERT предлагала в качестве борьбы с уязвимостью замену процессоров: «Уязвимость вызвана выборами при проектировании микропроцессоров. Полное удаление уязвимости требует замены уязвимых микропроцессоров». Однако в последующих текстах этот вариант исправления более не упоминался[11].