
Spectre, одна из недавно обнаруженных уязвимостей, основана на использовании инструкций косвенной передачи управления. Техника под названием Branch target injection способна преодолевать изоляцию между параллельно выполняемыми потоками. В ее основе лежит тот факт, что предиктор (блок предсказания ветвлений) управляет приоритетным выбором инструкций для опережающего (спекулятивного) выполнения и действует на основании общей статистики, полученной при анализе группы потоков, без ее дифференцирования по каждому из них.
Пройти сквозь стену
Сказанное выше в первую очередь касается систем с Hyper-Threading. Атакующий поток, реализуя управляемый сценарий с инструкциями косвенного перехода, может влиять на действия предиктора в другом, атакуемом потоке. Внимательный читатель найдет здесь противоречие: результаты, полученные при опережающем выполнении, бесследно уничтожаются в тот момент, когда выясняется, что адрес был предсказан неверно так как спекулятивно выполненный код не получает управления по логике программы.
Проблема в том, что спекулятивно выполненный код может оставить некий след, например, кэшировать один из элементов некоторого массива. Затем, сравнив время чтения нескольких элементов массива, атакующая программа теоретически способна определить адрес кэшированного элемента (он прочитается быстрее) и конвертировать его в нужную информацию.
Построить стену, через которую не пройти
Итак, уязвимое место найдено — это инструкции косвенного перехода по адресу, находящемуся в регистре или ячейке памяти. Чем заменить такие команды?
Наряду с простейшей конструкцией, состоящей из инструкций PUSH и RET, была предложена более сложная технология, получившая название «retpoline» или return trampoline. Метод основан на вызове подпрограммы, внутри которой происходит модификация адреса возврата, находящегося в стеке. Адрес возврата устанавливается равным входному параметру подпрограммы. В результате, возврат из такой подпрограммы будет осуществлен не на инструкцию, находящуюся после CALL, а в точку, заданную входным параметром. Вызвав такую подпрограмму и передав ей целевой адрес перехода, мы получаем аналог инструкции косвенного перехода, теоретически защищенный от атак, основанных на спекулятивном выполнении.
Рассмотрим один из примеров. Заметим, что в оригинальном тексте применяется ассемблерная нотация AT&T, но мы будем использовать нотацию Intel, предполагая, что процессор работает в 64-битном режиме.
Анализируем код
Рассмотрим инструкцию косвенного перехода, передающую управление по адресу, находящемуся в регистре r11: jmp r11
Модифицированный защищенный вариант:
call set_up_target
capture_spec: pause
jmp capture_spec
set_up_target: mov [rsp],r11
ret
Прокомментируем по инструкциям:
; вызываем подпрограмму, в регистре r11 целевой адрес косвенного перехода
call set_up_target
; метка, эта точка получит управление только спекулятивно, по логике программы, выполнение сюда не никогда придет
capture_spec:
; минимизировать утилизацию процессорных ресурсов и влияние на соседний поток.
pause
; вечный цикл на инструкцию pause
jmp capture_spec
; метка, адрес, по которому начинается вызванная подпрограмма
set_up_target:
; подпрограмма изменяет адрес возврата в стеке, это по-прежнему несовместимо с технологией Shadow stacks
mov [rsp],r11
; возврат выполнит переход по измененному адресу
ret
Резюме
Поскольку эта техника также использует модификацию содержимого стека, несовместимость с механизмом теневых стеков, описанная по первой ссылке в начале статьи, остается в силе. Можно предположить, что для новых процессоров, оборудованных аппаратной защитой в виде Control Flow Enforcement Technology, частью которой и является Shadow Stacks, такие меры не потребуются, что и нивелирует противоречие. Следует ожидать, что потери производительности при использовании описанных конструкций будут зависеть от модели процессора и контекста выполнения.
Описанный вариант, в сравнении с PUSH-RET содержит больше процессорных команд, но вместе с тем, не нарушает парность операций CALL и RETURN. Еще осторожнее будем в прогнозах относительно защищенности, описанный вариант оценим как более предпочтительный в сравнении с PUSH-RET, в силу «усмирения» предиктора инструкцией PAUSE, предписывающей потоку освободить максимум исполнительных ресурсов.
Принципиально, что в рассмотренном фрагменте кода, инструкция PAUSE и повторяющий ее «вечный цикл» находятся в спекулятивной ветке, согласно логике выполнения программы, управление на нее не передается, но с точки зрения предиктора, эта точка является наивероятнейшей точкой продолжения.