Главная » Статьи о компьютерах » Что такое спекулятивное выполнение?

Что такое спекулятивное выполнение?

Актуальные сегодня сценарии поведения вредоносного кода (Meltdown и Spectre) основаны на спекулятивном выполнении инструкций современными процессорами и, что особенно важно игнорировании ряда защитных механизмов при таком выполнении. Спекулятивное, или опережающее исполнение позволяет повысить производительность, оптимизируя загрузку операционных блоков CPU. Отметим, что опережающее исполнение распространяется не только на линейные участки программы, но и условные переходы а также косвенные формы передачи управления, а именно переход по адресу, находящемуся в регистре или ячейке памяти.

Чтобы точки ветвления программного кода не останавливали его опережающее выполнение, используется логика предсказания ветвлений. Собирая статистику о ранее выполненных ветвлениях, процессор определяет наиболее вероятные целевые адреса, отдавая им приоритет при опережающем исполнении инструкций, в расчете на то, что после условного или косвенного перехода, выполнение программы продолжится по предсказанному адресу. В этом случае результаты заблаговременно выполненных действий будут востребованы.

Но при любом предсказании возможны ошибки. Если условный или косвенный переход состоялся по адресу, отличному от предсказанного (branch misprediction) и заблаговременно выполненная ветка программы не получила управления, то результаты спекулятивного выполнения должны быть уничтожены. Очевидно, это отразится на производительности, но не приведет к ошибкам при выполнении программы, в силу того, что результаты, ставшие не нужными не окажут влияния на архитектурное состояние CPU.

Почему спекулятивное выполнение считалось безопасным?

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

В зависимости от того, верным или ошибочным было предсказание, приведшее к преждевременному исполнению запрещенной операции, возможно два варианта развязки.

  1. Предсказание было неверным. Это означает, что согласно логике программы, ветка, содержащая недопустимую операцию, не получает управления. В этом случае результат будет уничтожен и не окажет влияния на архитектурное (программно-видимое) состояние процессора. Вмешательство защитных механизмов здесь не требуется.
  2. Предсказание было верным. Это означает, что согласно логике программы, недопустимая операция действительно должна быть выполнена. В этом случае, требуется вмешательство защитных механизмов и произойдет оно в тот момент, когда результаты незаконных действий должны стать программно видимыми. В приведенном примере, нарушается защита страниц, поэтому реакцией процессора станет генерация исключения Page Fault с номером 14=0Eh. В результате управление будет принудительно передано от вредоносного приложения к процедуре обработки ошибки, являющейся частью операционной системы, а незаконно прочитанный байт никогда не окажется в регистре-получателе и не станет программно-видимым, даже для привилегированного кода, обрабатывающего ошибку нарушения защиты.

На первый взгляд оба варианта безопасны, несмотря на то, что в течение некоторого времени (доли микросекунды) спекулятивно выполненный код располагает незаконно прочитанным байтом.

Почему спекулятивное выполнение стало опасным?

Можно ли «пройти сквозь стену» и передать содержимое незаконно прочитанного байта из спекулятивной ветви выполнения в нормальную.

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

Один из вариантов: создать в общедоступном пользовательском пространстве памяти измерительный массив. Выполнив в спекулятивном коде обращение к одному элементу массива, адрес которого зависит от содержимого запретного байта, автор вредоносного кода обеспечит кэширование этого элемента. Далее, в нормальной ветке следует сравнить время чтения элементов массива, воспользовавшись прецизионным таймером, например счетчиком процессорных тактов Time Stamp Counter (TSC). Предварительно кэшированный элемент прочитается быстрее остальных. Зная его адрес, вычисляем содержимое запретного байта.

На практике, значение запретного байта, перед использованием в качестве индекса для адресации измерительного массива, разумно умножить на размер страницы памяти (4 килобайта или 4096 байт), что равносильно сдвигу на 12 бит влево. Такой прием позволяет задействовать в измерительном механизме кэш трансляции страниц (TLB или Translation Lookaside Buffer), что, по сравнению с кэш-памятью данных обеспечит более стабильный результат, поскольку каждой помечаемой ячейке будет соответствовать собственная страница памяти.

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


26.02.2018