В статье, упомянутой sgbj в комментариях, написанных Полом Тернером из Google, гораздо более подробно объясняется следующее, но я вкратце расскажу об этом:
Насколько я могу собрать это воедино из ограниченной информации на данный момент, ретполин является возвратным батутом, который использует бесконечный цикл, который никогда не выполняется, чтобы не допустить спекуляции ЦП о цели косвенного прыжка.
Базовый подход можно увидеть в ветке ядра Энди Клин, решающей эту проблему:
Он вводит новый __x86.indirect_thunk
вызов, который загружает цель вызова, чей адрес памяти (который я буду называть ADDR
) хранится в верхней части стека, и выполняет переход с помощью RET
инструкции. Затем сам блок вызывается с помощью макроса NOSPEC_JMP / CALL , который использовался для замены многих (если не всех) косвенных вызовов и переходов. Макрос просто помещает цель вызова в стек и корректно устанавливает адрес возврата, если необходимо (обратите внимание на нелинейный поток управления):
.macro NOSPEC_CALL target
jmp 1221f /* jumps to the end of the macro */
1222:
push \target /* pushes ADDR to the stack */
jmp __x86.indirect_thunk /* executes the indirect jump */
1221:
call 1222b /* pushes the return address to the stack */
.endm
Размещение call
в конце необходимо, чтобы, когда косвенный вызов завершился, поток управления продолжался за использованием NOSPEC_CALL
макроса, поэтому его можно было использовать вместо обычногоcall
Сам Thunk выглядит следующим образом:
call retpoline_call_target
2:
lfence /* stop speculation */
jmp 2b
retpoline_call_target:
lea 8(%rsp), %rsp
ret
Поток управления может немного запутаться, поэтому позвольте мне уточнить:
call
выталкивает текущий указатель инструкции (метка 2) в стек.
lea
добавляет 8 к указателю стека , фактически отбрасывая последнее введенное четырехзначное слово, которое является последним адресом возврата (для метки 2). После этого вершина стека снова указывает на реальный адрес возврата ADDR.
ret
переходит *ADDR
и сбрасывает указатель стека на начало стека вызовов.
В конце концов, все это поведение практически эквивалентно прыгать прямо *ADDR
. Единственное преимущество, которое мы получаем, заключается в том, что предиктор ветвления, используемый для операторов возврата (Return Stack Buffer, RSB), при выполнении call
инструкции предполагает, что соответствующий ret
оператор перейдет к метке 2.
Часть после метки 2 фактически никогда не выполняется, это просто бесконечный цикл, который теоретически заполнил бы конвейер JMP
инструкций инструкциями. При использовании LFENCE
, PAUSE
или в более общем случае инструкции в результате чего конвейера команд будет срыв останавливает процессор от тратя сил и времени на этом спекулятивном выполнении. Это потому, что в случае, если вызов retpoline_call_target будет возвращаться нормально, это LFENCE
будет следующая инструкция, которая будет выполнена. Это также то, что предсказатель ветви будет предсказывать на основе исходного адреса возврата (метка 2).
Цитировать из руководства по архитектуре Intel:
Инструкции, следующие за LFENCE, могут быть извлечены из памяти до LFENCE, но они не будут выполняться, пока LFENCE не завершится.
Однако обратите внимание, что в спецификации никогда не упоминается, что LFENCE и PAUSE приводят к остановке конвейера, поэтому я читаю немного между строк здесь.
Теперь вернемся к исходному вопросу: раскрытие информации о памяти ядра возможно благодаря комбинации двух идей:
Даже если спекулятивное выполнение должно быть свободным от побочных эффектов, если спекуляция была неправильной, спекулятивное выполнение по-прежнему влияет на иерархию кэша . Это означает, что, когда загрузка памяти выполняется спекулятивно, это все равно могло привести к удалению строки кэша. Это изменение в иерархии кеша можно определить путем тщательного измерения времени доступа к памяти, которая отображается на тот же набор кеша.
Вы даже можете потерять некоторые биты произвольной памяти, когда адрес источника чтения памяти сам считывался из памяти ядра.
Косвенный предсказатель ветвления процессоров Intel использует только самые младшие 12 битов исходной команды, поэтому легко отравить все 2 ^ 12 возможных историй предсказания с адресами памяти, управляемыми пользователем. Затем они могут, когда в ядре прогнозируется косвенный переход, спекулятивно выполняться с привилегиями ядра. Используя побочный канал тайминга кеша, вы можете утечь произвольную память ядра.
ОБНОВЛЕНИЕ: В списке рассылки ядра идет постоянное обсуждение, которое наводит меня на мысль, что retpolines не полностью смягчают проблемы предсказания ветвлений, например, когда буфер возврата стека (RSB) работает пусто, более поздние архитектуры Intel (Skylake +) отступают. в уязвимый целевой буфер филиала (BTB):
Ретполин как стратегия смягчения меняет косвенные ответвления на возвраты, чтобы избежать использования прогнозов, исходящих от BTB, так как они могут быть отравлены злоумышленником. Проблема с Skylake + заключается в том, что недостаточный уровень RSB возвращается к использованию предсказания BTB, которое позволяет атакующему контролировать спекуляции.