Что такое ретполин и как он работает?
Для предотвращения раскрытия памяти в ядре или межпроцессном режиме (атака Spectre) ядро Linux1 будет скомпилировано с новой опцией: -mindirect-branch=thunk-extern
представил gcc
выполнять косвенные вызовы через так называемый ретполин.
Похоже, что это новый изобретенный термин, поскольку поиск в Google обнаруживает только очень недавнее использование (как правило, все в 2018 году).
Что такое ретполин и как он предотвращает недавние атаки по раскрытию информации ядра?
1 Однако, это не специфично для Linux - похоже, что аналогичная или идентичная конструкция используется как часть стратегии смягчения в других ОС.
4 ответа
В статье, упомянутой 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
Сам ствол выглядит следующим образом:
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 возможных историй предсказания с адресами памяти, управляемыми пользователем. Затем они могут, когда в ядре прогнозируется косвенный переход, спекулятивно выполняться с привилегиями ядра. Используя побочный канал тайминга кеша, вы можете утечь произвольную память ядра.
ОБНОВЛЕНИЕ: В списке рассылки ядра идет постоянное обсуждение, которое наводит меня на мысль, что ретполины не полностью смягчают проблемы предсказания ветвлений, например, когда буфер возврата стека (RSB) работает пусто, более поздние архитектуры Intel (Skylake+) отступают. в уязвимый целевой буфер филиала (BTB):
Ретполин в качестве стратегии смягчения заменяет косвенные ответвления на возвраты, чтобы избежать использования прогнозов, исходящих от BTB, так как они могут быть отравлены злоумышленником. Проблема с Skylake + заключается в том, что недостаточный уровень RSB возвращается к использованию предсказания BTB, которое позволяет атакующему контролировать спекуляции.
Ретполин предназначен для защиты от эксплойта с использованием целевой инъекции (CVE-2017-5715). Это атака, когда косвенная инструкция ветвления в ядре используется для принудительного выполнения произвольного фрагмента кода. Выбранный код представляет собой "гаджет", который так или иначе полезен для злоумышленника. Например, можно выбрать код, чтобы утечка данных ядра влияла на кеш. Retpoline предотвращает этот эксплойт, просто заменяя все косвенные инструкции ветвления инструкцией возврата.
Я думаю, что ключ к retpoline - это просто часть "ret", которая заменяет непрямую ветвь инструкцией возврата, так что CPU использует предиктор стека возврата вместо эксплуатируемого предиктора ветвления. Если вместо этого использовать простую команду push и return, то код, который будет спекулятивно выполняться, будет кодом, в который функция в конечном итоге вернется, а не каким-нибудь гаджетом, полезным для атакующего. Основным преимуществом батутной части, похоже, является поддержание стека возврата, поэтому, когда функция действительно возвращается к своему вызывающему, это предсказывается правильно.
Основная идея внедрения целевой ветви проста. Он использует тот факт, что ЦП не записывает полный адрес источника и места назначения ветвей в своих целевых буферах ветвей. Таким образом, злоумышленник может заполнить буфер, используя переходы в своем собственном адресном пространстве, что приведет к предсказанным попаданиям при выполнении определенного косвенного перехода в адресном пространстве ядра.
Обратите внимание, что retpoline не предотвращает раскрытие информации о ядре напрямую, а только предотвращает использование косвенных инструкций ветвления для спекулятивного выполнения гаджета, который раскрыл бы информацию. Если злоумышленник может найти другие способы для умозрительного выполнения гаджета, то ретполина не предотвратит атаку.
В документе " Атака на призрак: использование спекулятивного исполнения " Пола Кохера, Даниэля Генкина, Даниэля Грусса, Вернера Хааса, Майка Гамбурга, Морица Липпа, Стефана Мангарда, Томаса Прешера, Майкла Шварца и Ювала Ярома дается следующий обзор того, как косвенные ветви могут быть использованы:
Использование косвенных веток. Рисуя из ориентированного на возврат программирования (ROP), в этом методе злоумышленник выбирает гаджет из адресного пространства жертвы и влияет на жертву для умозрительного выполнения гаджета. В отличие от ROP, злоумышленник не полагается на уязвимость в коде жертвы. Вместо этого злоумышленник обучает целевой буфер ветвления (BTB) неправильно предсказывать переход от косвенной инструкции ветвления к адресу гаджета, что приводит к спекулятивному выполнению гаджета. В то время как спекулятивно выполненные инструкции отменяются, их влияние на кеш не отменяется. Эти эффекты могут использоваться гаджетом для утечки конфиденциальной информации. Мы покажем, как при тщательном выборе гаджета этот метод можно использовать для чтения произвольной памяти жертвы.
Чтобы неправильно настроить BTB, злоумышленник находит виртуальный адрес гаджета в адресном пространстве жертвы, а затем выполняет косвенные переходы по этому адресу. Это обучение выполняется из адресного пространства злоумышленника, и не имеет значения, что находится по адресу гаджета в адресном пространстве злоумышленника; все, что требуется, - это то, что ветвь, используемая для обучения ветвей, использует один и тот же виртуальный адрес назначения. (На самом деле, пока злоумышленник обрабатывает исключения, атака может работать, даже если в виртуальном адресе гаджета в адресном пространстве злоумышленника не сопоставлен код.) Также нет необходимости в полном совпадении исходного адреса филиала, используемого для обучения, и адрес целевого филиала. Таким образом, злоумышленник обладает значительной гибкостью в настройке обучения.
Запись в блоге, озаглавленная " Чтение привилегированной памяти с побочным каналом" командой Project Zero в Google, предоставляет еще один пример того, как внедрение целевого объекта ветвления можно использовать для создания рабочего эксплойта.
Этот вопрос был задан некоторое время назад, и заслуживает более нового ответа.
Последовательности "Retpoline" - это программная конструкция, которая позволяет изолировать непрямые ветви от спекулятивного исполнения. Это может применяться для защиты чувствительных двоичных файлов (таких как реализации операционной системы или гипервизора) от атак внедрения целевых ветвей против их косвенных ветвей.
Слово " ret poline " является портом слов "return" и "trampoline", во многом как улучшение " rel poline ", придуманное из "относительного вызова" и "батута". Это батутная конструкция, построенная с использованием операций возврата, которая также образно гарантирует, что любое связанное с ней умозрительное выполнение будет бесконечно "подпрыгивать".
Для предотвращения раскрытия памяти в ядре или в межпроцессном режиме (атака Spectre) ядро Linux [1] будет скомпилировано с новой опцией:
-mindirect-branch=thunk-extern
введен в gcc для выполнения косвенных вызовов через так называемый ретполин.[1] Однако, это не специфично для Linux - похоже, что аналогичная или идентичная конструкция используется как часть стратегии смягчения в других ОС.
Использование этой опции компилятора защищает только от Spectre V2 в уязвимых процессорах, для которых требуется обновление микрокода, необходимое для CVE-2017-5715. Он будет " работать " с любым кодом (не только с ядром), но стоит атаковать только код, содержащий "секреты".
Похоже, что это новый изобретенный термин, поскольку поиск в Google обнаруживает только очень недавнее использование (как правило, все в 2018 году).
Компилятор LLVM имеет -mretpoline
переключиться с 4 января 2018 года. Это дата, когда уязвимость была впервые опубликована. GCC выпустила свои патчи 7 января 2018 года.
Дата CVE предполагает, что уязвимость была " обнаружена " в 2017 году, но она затрагивает некоторые процессоры, изготовленные в последние два десятилетия (таким образом, она, вероятно, была обнаружена давно).
Что такое ретполин и как он предотвращает недавние атаки по раскрытию информации ядра?
Сначала несколько определений:
Батут - иногда упоминаемые как непрямые векторы прыжков - это места памяти, содержащие адреса, указывающие на процедуры обработки прерываний, процедуры ввода-вывода и т. Д. Выполнение прыгает в батут, а затем сразу же выпрыгивает или подпрыгивает, отсюда и термин батут. GCC традиционно поддерживает вложенные функции, создавая исполняемый батут во время выполнения, когда берется адрес вложенной функции. Это небольшой фрагмент кода, который обычно находится в стеке, в кадре стека содержащей функции. Батут загружает статический цепной регистр и затем переходит на реальный адрес вложенной функции.
Thunk - Thunk - это подпрограмма, используемая для добавления дополнительных вычислений в другую подпрограмму. Thunks в основном используются для задержки вычисления до тех пор, пока не понадобится его результат, или для вставки операций в начале или конце другой подпрограммы.
Memoization - запоминаемая функция "запоминает" результаты, соответствующие некоторому набору определенных входных данных. Последующие вызовы с запомненными входами возвращают запомненный результат, а не пересчитывают его, тем самым устраняя первичную стоимость вызова с заданными параметрами из всех, кроме первого вызова функции с этими параметрами.
Грубо говоря, ретполин - это батут с возвратом в виде снаряда, чтобы " испортить " запоминание в косвенном предсказателе ветвления.
Источник: retpoline включает инструкцию PAUSE для Intel, но инструкция AMD LFENCE необходима для AMD, так как на этом процессоре инструкция PAUSE не является командой сериализации, поэтому цикл pause/jmp будет использовать избыточную мощность, так как предполагается, что он ожидает возврата неправильно прогнозировать правильную цель.
У Arstechnica есть простое объяснение проблемы:
"Каждый процессор имеет архитектурное поведение (документированное поведение, описывающее, как работают инструкции и от которого программисты зависят при написании своих программ), и микроархитектурное поведение (поведение реальной реализации архитектуры). Они могут незначительно отличаться. Например, архитектурно, программа, которая загружает значение из определенного адреса в памяти, будет ждать, пока адрес не станет известен, прежде чем попытаться выполнить загрузку, однако, микроархитектурно процессор может попытаться спекулятивно угадать адрес, чтобы он мог начать загрузка значения из памяти (что происходит медленно) даже до того, как будет абсолютно точно определено, какой адрес ему следует использовать.
Если процессор угадает неправильно, он будет игнорировать предполагаемое значение и выполнит загрузку снова, на этот раз с правильным адресом. Архитектурно определенное поведение, таким образом, сохраняется. Но это ошибочное предположение будет мешать другим частям процессора, в частности содержимому кеша. Эти микроархитектурные нарушения могут быть обнаружены и измерены путем определения времени, необходимого для доступа к данным, которые должны (или не должны) быть в кеше, что позволяет вредоносной программе делать выводы о значениях, хранящихся в памяти ".
Из статьи Intel: " Ретполин: снижение уровня инъекций для целевого сектора " ( .PDF):
"Последовательность retpoline не позволяет спекулятивному выполнению процессора использовать" косвенный предиктор ветвления "(один из способов прогнозирования потока программы), чтобы спекулировать по адресу, управляемому с помощью эксплойта (удовлетворяя элементу 4 из пяти элементов внедрения цели ветвления (вариант Spectre 2)) использовать композицию, указанную выше).
Обратите внимание, что элемент 4: "Эксплойт должен успешно влиять на эту косвенную ветвь, чтобы умозрительно неправильно прогнозировать и выполнять гаджет. Этот гаджет, выбранный эксплойтом, пропускает секретные данные через побочный канал, как правило, из-за тайминга".
«Ретполайн» существует уже много лет на множестве различных процессоров, просто его никогда так не называли. Это старый трюк, который злоупотребляет принципом работы аппаратного стека на большинстве машин. Чтобы понять это полностью, вам необходимо знать следующие термины программирования на ассемблере:
- Счетчик программ / указатель команд: регистр процессора, содержащий следующую команду, которая должна быть выполнена. Его значение обновляется после обработки каждой инструкции.
- Стек: структура данных, к которой оборудование может получить доступ, вставив в нее значения. В
POP
операция извлекает последний элемент, помещенный в стек, и сохраняет его в выбранном регистре. Например,pop %eax
возьмет верхние 4 байта из стека и сохранит их в регистре%eax
.
Когда вы вызываете функцию, например, написанную на C:
y = foo(x)
, процессор выполняет инструкцию в ячейке памяти функции.
CALL foo
сохранит значение указателя инструкции
PUSH
помещает его в стек, а затем выполняет
JMP
(по сути а) к. Этот помещенный указатель инструкции известен как адрес возврата.
Тело
foo
закончится инструкцией под названием. Это берет верхние (независимо от размера указателей вашей машины) байты из стека и загружает их непосредственно в указатель инструкции, что делает
GOTO
по этому адресу по существу. Имейте в виду, что ЦП не имеет возможности узнать, являются ли верхние байты стека адресом возврата!Нажав произвольную последовательность байтов, а затем выполнив
RET
инструкцию вы можете заставить CPU прыгать где угодно.
Все еще со мной? Хороший. Я упомянул, что эта техника существует уже давно. Он в основном использовался программами, написанными непосредственно на ассемблере для машин для реализации структур корпуса переключателя, поскольку многие 8-битные и 16-битные процессоры не могли
CALL
все, что не было постоянным адресом памяти, встроенным непосредственно в исходный код. Языки высокого уровня управляют стеком автоматически и не слишком любят ретполинии, поскольку они склонны изменять стек для хранения локальных переменных и ожидают, что он всегда будет в определенном состоянии.
Так уж получилось, что этот старый трюк снова стал полезным, но по совершенно другой причине, чем раньше.