Как построить стену для Spectre?

29 Янв 2018

Как построить стену для Spectre?

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 в документации Intel

Принципиально, что в рассмотренном фрагменте кода, инструкция PAUSE и повторяющий ее «вечный цикл» на­хо­дят­ся в спекулятивной ветке, согласно логике выполнения программы, управление на нее не передается, но с точ­ки зрения предиктора, эта точка является наивероятнейшей точкой продолжения.