Внутренняя работа Призрака (v2)
Я немного читал о Spectre v2, и, очевидно, вы получите нетехнические объяснения. У Питера Кордеса есть более глубокое объяснение, но он не полностью рассматривает некоторые детали. Примечание: я никогда не проводил атаку Spectre v2, поэтому у меня нет опыта. Я только прочитал о теории.
Я понимаю, что Spectre v2 заключается в том, что вы делаете ошибочный прогноз, например, о косвенной ветке if (input < data.size)
, Если косвенный целевой массив (в котором я не слишком уверен в деталях - то есть, почему он отделен от структуры BTB) - который перепроверяется при декодировании для RIP непрямых ветвлений - не содержит прогноз, тогда он вставит новый прыжок RIP (выполнение ветви в конечном итоге вставит целевой RIP ветви), но пока он не знает целевой RIP прыжка, поэтому любая форма статического предсказания не будет работать. Насколько я понимаю, он всегда будет предсказывать, что он не будет принят для новых непрямых ветвей, и когда порт 6 в конечном итоге разработает целевой RIP перехода и прогнозирования, он откатится с помощью BOB и обновит ITA с правильным адресом перехода, а затем обновит локальный и глобальная отраслевая история регистрирует и насыщающие счетчики соответственно.
Хакер должен натренировать насыщающие счетчики, чтобы всегда предсказать, что, я думаю, они делают, запустив if(input < data.size)
несколько раз в цикле, где input
настроен на то, что действительно меньше data.size
(соответственно перехват ошибок) и на последней итерации цикла сделайте input
больше, чем data.size
(Например, 1000); будет предсказано, что косвенная ветвь будет принята, и она перейдет к телу оператора if, где происходит загрузка кэша.
Оператор if содержит secret = data[1000]
(Конкретный адрес памяти (данные [1000]), который содержит секретные данные, предназначен для загрузки из памяти в кэш), затем он будет спекулятивно распределен в буфере загрузки. Предыдущая непрямая ветвь все еще находится в модуле выполнения ветвлений и ожидает завершения.
Я считаю, что предпосылка заключается в том, что нагрузка должна быть выполнена (назначен буфер заполнения строки), прежде чем буферы загрузки будут сброшены при неправильном прогнозировании. Если ему уже был назначен буфер заполнения строки, то ничего не поделаешь. Имеет смысл, что нет механизма отмены выделения буфера заполнения строки, потому что буфер заполнения строки должен был бы отложиться перед сохранением в кэш после возвращения его в буфер загрузки. Это может привести к насыщению буферов заполнения строк, потому что вместо освобождения при необходимости (сохранение его там для скорости других загрузок по тому же адресу, но освобождение при отсутствии других доступных строчных буферов). Он не сможет освободить место до тех пор, пока не получит некоторый сигнал о том, что сброс не произойдет, то есть он должен прерваться для выполнения предыдущей ветви, вместо того, чтобы немедленно сделать буфер заполнения строки доступным для хранилищ другого логического ядра. Этот механизм сигнализации может быть сложным для реализации, и, возможно, он не приходил им в голову (предварительное мышление), и он также привел бы к задержке в том случае, если выполнение ветвлений занимает достаточно времени для зависания буферов заполнения строки, что может повлиять на производительность, т.е. если data.size
целенаправленно сбрасывается из кеша (CLFLUSH
) до последней итерации цикла, означающего, что выполнение ветвления может занять до 100 циклов.
Я надеюсь, что мое мышление правильно, но я не уверен на 100%. Если у кого-то есть что-то добавить или исправить, пожалуйста, сделайте
3 ответа
Иногда термин "BTB" используется вместе для обозначения всех буферов, используемых модулем прогнозирования ветвлений. Однако на самом деле существует несколько буферов, каждый из которых используется в каждом цикле для прогнозирования цели и направления. В частности, BTB используется для прогнозирования прямых ветвлений, ITB (косвенный целевой буфер) используется для прогнозирования косвенных ветвлений, за исключением возвратов, а RSB используется для прогнозирования возвратов. ITB также называется IBTB или косвенным целевым массивом. Все эти термины используются разными поставщиками и исследователями. Как правило, BTB используется для создания начальных прогнозов для всех видов команд ветвления, когда другие буферы отсутствуют. Но позже предиктор узнает больше о ветвях, и другие буферы вступают в игру. Если несколько динамических экземпляров одной и той же косвенной ветви имеют одну и ту же цель, то вместо ITB может также использоваться BTB. ITB гораздо точнее, когда одна и та же ветвь имеет несколько целей и предназначена специально для работы с такими ветвями. См.: Предсказание веток и выступления переводчиков - Не верь фольклору. Первым процессором Intel, в котором реализованы отдельные структуры BTB и ITB, является Pentium M. Все последующие процессоры Intel Core имеют выделенные ITB.
Эксплойт Spectre V1 основан на обучении BTB с использованием программы злоумышленника, так что, когда жертва выполняет ветвь, которая псевдоним той же записи BTB, процессор обманом умело выполняет инструкции (называемые гаджетом) для утечки информации. Эксплойт Spectre V2 аналогичен, но вместо этого основан на обучении ITB. Принципиальное отличие здесь состоит в том, что в V1 процессор неверно предсказывает направление ветви, в то время как в V2 процессор неверно предсказывает цель ветви (и, в случае условной косвенной ветви, также направление, потому что мы хотим, чтобы оно было быть взятым). В программах, которые интерпретируются, JIT-компилируются или используют динамический полиморфизм, может быть много косвенных ветвей (кроме возвратов). Конкретная непрямая ветвь никогда не может быть предназначена для перехода в какое-то место, но, неправильно обучив предиктора, она может быть сделана, чтобы прыгнуть куда угодно. Именно по этой причине V2 очень мощный; Независимо от того, где находится гаджет и какие бы намеренные потоки управления программы не находились, вы можете выбрать одну из косвенных ветвей и заставить ее спекулятивно перейти к гаджету.
Обратите внимание, что обычно линейный адрес цели статической прямой ветви остается неизменным на протяжении всего жизненного цикла программы. Есть только одна ситуация, когда это может быть не так: динамическая модификация кода. Так что, по крайней мере, теоретически, эксплойт Spectre может быть разработан на основе целевого неверного предсказания прямых ветвей.
Что касается восстановления LFB, я не очень понимаю, что вы говорите. Когда запрос на загрузку, который пропустил L1D, получает данные в LFB, данные немедленно передаются в обходное соединение конвейера. Должен быть способ определить, какой тип загрузки запросил эти данные. Возвращаемые данные должны быть помечены UOP ID загрузки. Источники мопов в RS, которые ожидают данные, представлены в виде идентификаторов моп нагрузок. Кроме того, запись ROB, в которой хранится загрузка, должна быть помечена как завершенная, чтобы ее можно было удалить, а в pre-SnB возвращенные данные необходимо записать в ROB. Если при очистке конвейера невыполненный запрос на загрузку в LFB не отменяется, и если идентификатор загрузки uop повторно использовался для некоторого другого uop, когда данные поступают, он может быть неправильно перенаправлен на все новые операции в настоящее время в конвейере, тем самым портит микроархитектурное состояние. Таким образом, должен быть способ гарантировать, что это не произойдет ни при каких обстоятельствах. Очень возможно отменить невыполненные запросы на загрузку и спекулятивные RFO при очистке конвейера, просто пометив все действительные записи LFB как "отмененные", просто чтобы данные не возвращались в конвейер. Тем не менее, данные все еще могут быть получены и заполнены в один или несколько уровней кэшей. Запросы в LFB идентифицируются физическими адресами с выравниванием строк. Могут быть и другие возможные конструкции.
Я решил провести эксперимент, чтобы точно определить, когда LFB будут освобождены на Haswell. Вот как это работает:
Outer Loop (10K iterations):
Inner Loop (100 iterations):
10 load instructions to different cache lines most of which miss the L2.
LFENCE.
A sequence of IMULs to delay the resolution of the jump by 18 cycles.
Jump to inner.
3 load instructions to different cache lines.
LFENCE.
Jump to outer.
Чтобы это работало, гиперпоточность и оба устройства предварительной выборки L1 должны быть отключены, чтобы гарантировать, что мы владеем всеми 10 LFB L1.
LFENCE
инструкции гарантируют, что мы не исчерпаем LFB при выполнении по правильно предсказанному пути. Ключевая идея здесь заключается в том, что внутренний скачок будет неверно предсказан один раз за внешнюю итерацию, поэтому в LFB можно выделить до 10 нагрузок внутренней итерации, которые находятся на неверно предсказанном пути. Обратите внимание, что LFENCE
предотвращает распределение нагрузок от последующих итераций. Через несколько циклов внутренняя ветвь будет разрешена, и произойдет неправильное предсказание. Конвейер очищается, а внешний интерфейс извлекается для извлечения и выполнения инструкций загрузки во внешнем цикле.
Есть два возможных результата:
- LFB, которые были выделены для нагрузок на неверно предсказанном пути, немедленно освобождаются как часть операции очистки конвейера и становятся доступными для других нагрузок. В этом случае не будет срывов из-за недоступности LFB (подсчитывается с использованием
L1D_PEND_MISS.FB_FULL
). - LFB освобождаются только тогда, когда нагрузки обслуживаются независимо от того, были ли они на неверно предсказанном пути.
Если во внутреннем контуре после внутреннего скачка есть три нагрузки, измеренное значение L1D_PEND_MISS.FB_FULL
примерно равно числу внешних итераций. Это один запрос на итерацию внешнего цикла. Это означает, что когда три нагрузки на правильном пути передаются на L1D, нагрузки от неверно указанного пути все еще занимают 8 записей LFB, что приводит к событию заполнения FB для третьей загрузки. Это говорит о том, что нагрузка в LFBs де лакируется только тогда, когда нагрузка фактически завершается.
Если я добавлю менее двух загрузок во внешний цикл, в основном не будет событий FB full. Я заметил одну вещь: для каждой дополнительной нагрузки во внешнем контуре, превышающей три нагрузки, L1D_PEND_MISS.FB_FULL
увеличивается примерно на 20К вместо ожидаемых 10К. Я думаю, что происходит то, что, когда запрос на загрузку UOP нагрузки выдается в L1D впервые, и все LFB используются, он отклоняется. Затем, когда LFB становится доступным, две нагрузки, ожидающие в буфере загрузки, отправляются на L1D, одна будет распределена в LFB, а другая будет отклонена. Таким образом, мы получаем два события LFB full на каждую дополнительную нагрузку. Однако, когда во внешнем цикле три нагрузки, только третья будет ожидать LFB, поэтому мы получаем одно событие на каждую итерацию внешнего цикла. По существу, буфер загрузки не может различить наличие одного LFB или двух LFB; он только узнает, что по крайней мере один LFB свободен, и поэтому он пытается отправить два запроса на загрузку одновременно, поскольку есть два порта загрузки.
Для веток некоторые похожи jc .somewhere
где ЦП действительно нужно только угадать, будет ли взята ветвь или нет, чтобы можно было спекулировать на предполагаемом пути. Тем не менее, некоторые ветви похожи jmp [table+eax*8]
где может быть более 4 миллиардов возможных направлений, и для этих случаев ЦП должен угадать целевой адрес, чтобы можно было спекулировать по предполагаемому пути. Поскольку существуют очень разные типы ветвей, процессор использует очень разные типы предикторов.
Для Spectre есть "мета-паттерн" - злоумышленник использует умозрительное выполнение, чтобы обмануть процессор, оставляя информацию в чем-то, а затем извлекает эту информацию из чего-либо. Существует множество возможностей для "чего-то" (кэши данных, кэши инструкций, TLB, целевой буфер ветвления, буфер направления ветвления, стек возврата, буферы объединения записи, ...), и, следовательно, существует множество возможных вариаций спектра (и не только "хорошо известные первые два варианта", которые были обнародованы в начале 2018 года).
Для спектра v1 (где "что-то" является кешем данных) злоумышленнику нужен какой-то способ обмануть процессор, чтобы он помещал данные в кеш данных (например, нагрузка, а затем вторая загрузка, которая зависит от значения первой загрузки, что может быть выполненным умозрительно) и каким-то образом извлечь информацию (очистить все в кеше, затем использовать количество времени, которое загружается, чтобы определить, как изменилось состояние кеша данных).
Для спектра v2 (где "что-то" - буфер направления ветвления, используемый для таких инструкций, как jc .somewhere
) злоумышленнику нужен какой-то способ заставить процессор поместить данные в буфер направления ветвления (например, нагрузка, а затем ветвь, которая зависит от нагрузки, которая может выполняться спекулятивно) и какой-то способ извлечения информации (установить направление ветвления) заблаговременно перейдите в известное состояние, затем используйте количество времени, которое требуется ветви, чтобы определить, как изменилось состояние буфера направления ветви).
Для всех возможных вариаций спектра, единственно важным (для защиты) является то, что может быть "чем-то" (и как предотвратить попадание информации во "что-то", или сбросить / перезаписать / уничтожить информацию, попавшую в "что-то"). Все остальное (конкретные детали одной из множества возможных реализаций кода для атаки на любой из множества возможных вариантов призрака) не имеет значения.
Смутная история Призрака
Первоначальный Спектр (v1, с использованием тайминга кеша) был найден в 2017 году и публично объявлен в январе 2018 года. Это было похоже на прорыв плотины, и вскоре последовали несколько других вариантов (например, v2, с использованием предсказания ветвления). Эти ранние вариации привлекли много внимания. Примерно через 6 месяцев после этого было найдено множество других вариантов, но они не получили такой широкой огласки, и многие люди не знали (и до сих пор не знают) о них. Ко второй половине 2018 года люди (например, я) начали терять представление о том, какие варианты были доказаны (с помощью реализаций "доказательства концепции"), а какие еще не доказаны, и некоторые исследователи начали пытаться перечислять возможности и устанавливать соглашения об именах. для них. Лучший пример этого, который я видел до сих пор, - "Систематическая оценка атак и защит от временных действий" (см. https://arxiv.org/pdf/1811.05441.pdf).
Тем не менее, "дыра в стене плотины" - это не то, что можно легко закрыть, и (для случайных догадок) я думаю, что пройдет несколько лет, прежде чем мы сможем предположить, что все возможности были изучены (и я думаю, что необходимость смягчение никогда не исчезнет).
Спасибо Брендану и Хади Брэйс, прочитав ваши ответы и, наконец, прочитав призрачную статью, теперь ясно, где я ошибался в своих мыслях, и я немного перепутал их.
Я частично описывал Spectre v1, который вызывает обход проверки границ путем неправильного изучения истории перехода, т.е. if (x < array1_size)
гаджету призрака. Это явно не косвенная ветвь. Хакер делает это, вызывая функцию, содержащую гаджет-призрак с допустимыми параметрами, чтобы простить предиктор ветвления (PHT+BHT), а затем вызвать недопустимые параметры, чтобы принести array1[x]
в кеш. Затем они выписывают историю ветви, предоставляя правовые параметры, а затем сбрасывают array1_size
из кэша (что я не уверен, как они делают, потому что, даже если злоумышленник знает, VA array1_size
строка не может быть очищена, потому что TLB содержит другой PCID для процесса, поэтому он должен быть каким-либо образом исключен, то есть заполнить набор по этому виртуальному адресу). Затем они вызывают те же недопустимые параметры, что и раньше, и как array1[x]
в кеше но array1_size
не является, array[x]
быстро разрешится и начнется загрузка array2[array1[x]]
пока еще жду array1_size
, который загружает позицию в array2
основываясь на секрете в любом х, который выходит за границы array1
, Затем злоумышленник вызывает функцию с допустимым значением x и временем вызова функции (я предполагаю, что злоумышленник должен знать содержимое array1
потому что, если array2[array1[8]]
приводит к более быстрому доступу, они должны знать, что в array1[8]
так как это секрет, но, конечно, этот массив должен содержать каждую 2^8-битную комбинацию справа).
Для Spectre v2, с другой стороны, требуется второй процесс атаки, который знает виртуальный адрес косвенной ветви в процессе-жертве, чтобы он мог отравить цель и заменить ее другим адресом. Если процесс атаки содержит инструкцию перехода, которая будет находиться в том же наборе, пути и теге в IBTB, что и косвенная ветвь жертвы, то он просто обучает эту инструкцию ветвления предсказывать принятые и переходить к виртуальному адресу, который оказывается адресом гаджет в процессе жертвы. Когда процесс-жертва сталкивается с косвенной ветвью, в IBTB находится неправильный целевой адрес из программы атаки. Крайне важно, чтобы это была косвенная ветвь, потому что ложности в результате переключения процесса обычно проверяются при декодировании, т. Е. Если цель ветвления отличается от цели в BTB для этого RIP, тогда он сбрасывает инструкции, извлеченные перед ним. Этого нельзя сделать с помощью косвенных ветвей, поскольку он не знает цель до этапа выполнения, и, следовательно, идея заключается в том, что выбранная косвенная ветвь зависит от значения, которое необходимо извлечь из кэша. Затем он переходит к этому целевому адресу, который является адресом гаджета и так далее, и так далее.
Злоумышленник должен знать исходный код процесса жертвы, чтобы идентифицировать гаджет, и он должен знать виртуальную машину, на которой он будет находиться. Я предполагаю, что это можно сделать, зная, где будет загружен код. Я полагаю, что.exes обычно загружаются, например, в x00400000, а затем в заголовке PE есть BaseOfCode.
Редактировать: я только что прочитал Приложение B к документу "Призрак", и это делает для хорошей реализации Windows Spectre v2.
В качестве подтверждения концепции мы создали простое целевое приложение, которое предоставляет услугу вычисления SHA1-хэша ключа и входного сообщения. Эта реализация состояла из программы, которая непрерывно запускает цикл, который вызывает Sleep(0), загружает входные данные из файла, вызывает криптографические функции Windows для вычисления хеша и печатает хеш при каждом изменении входных данных. Мы обнаружили, что
Sleep()
Вызов выполняется с данными из входного файла в регистрах ebx, edi и известным для атакующего значением для edx, т. е. содержимое двух регистров контролируется атакующим. Это входные критерии для типа гаджета Spectre, описанного в начале этого раздела.
Оно использует ntdll.dll
(.dll полный родных заглушек системных вызовов API) и kernel32.dll
(Windows API), которые всегда отображаются в виртуальном адресном пространстве пользователя по направлению ASLR (указанному в изображениях.dll), за исключением того, что физический адрес, вероятно, будет одинаковым из-за сопоставления представления при копировании при записи на страницу кэш. Косвенная ветвь к яду будет в Windows API Sleep()
функция в kernel32.dll
который, кажется, косвенно вызывает NtDelayExecution()
в ntdll.dll
, Затем злоумышленник выясняет адрес косвенной команды ветвления и отображает страницу, охватывающую адрес жертвы, которая содержит целевой адрес, в свое собственное адресное пространство и изменяет целевой адрес, сохраненный по этому адресу, на адрес гаджета, который, как он определил, находится где-то в другом месте. в той же или другой функции в ntdll.dll
(Я не совсем уверен (из-за ASLR), как злоумышленник наверняка знает, где отображается процесс жертвы kernel32.dll
а также ntdll.dll
в его адресном пространстве, чтобы найти адрес косвенной ветви в Sleep()
для жертвы. Приложение B утверждает, что они использовали "Простые операции указателя", чтобы найти косвенную ветвь и адрес, который содержит цель - как это работает, я не уверен). Затем потоки запускаются с одинаковым сродством жертвы (так что жертва и потоки неправильного обучения имеют гиперпоточность на одном физическом ядре), которые вызывают Sleep()
самим косвенно обучать его, который в контексте адресного пространства процесса взлома теперь будет переходить на адрес гаджета. Гаджет временно заменен ret
так что он возвращается из Sleep()
плавно. Эти потоки также выполнят последовательность перед косвенным переходом, чтобы имитировать, какой будет глобальная история ветвления жертвы, прежде чем столкнуться с косвенным переходом, чтобы полностью убедиться, что ветвление взято в легированную историю. Затем запускается отдельный поток с дополнением сродства потока жертвы, который многократно высвобождает адрес памяти жертвы, содержащий адрес перехода, чтобы гарантировать, что когда жертва сталкивается с косвенной ветвью, ей потребуется длительный доступ к ОЗУ для разрешения, что позволяет гаджет, чтобы спекулировать заранее, прежде чем пункт назначения ответвления можно будет проверить на соответствие BTB и сбросить конвейер. В JavaScript вытеснение выполняется загрузкой в один и тот же набор кеша, то есть кратным 4096. На этом этапе все потоки неправильного обучения, потоки выселения и жертвы все работают и зацикливаются. Когда жертва процесса вызывает цикл Sleep()
непрямая ветвь спекулирует на гаджете из-за записи IBTB, которую хакер отравил ранее. Пробный поток запускается с дополнением схожести потокового процесса жертвы (чтобы не мешать неправильной работе и истории ветвей жертвы). Исследующий поток изменяет заголовок файла, который использует процесс жертвы, в результате чего эти значения находятся в ebx
а также edi
когда Sleep()
называется означает, что исследующий поток может напрямую влиять на значения, хранящиеся в ebx
а также edi
, Гаджет "Призрак", разветвленный в примере, добавляет значение, сохраненное в [ebx+edx+13BE13BDh]
в edi
а затем загружает значение по адресу, хранящемуся в edi
и добавляет его с переносом в dl
, Это позволяет исследующему потоку узнать значение, хранящееся в [ebx+edx+13BE13BDh]
как будто он выбирает оригинал edi
0, то значение, к которому осуществляется доступ во второй операции, будет загружено из диапазона виртуальных адресов 0x0 - 0x255, и к этому времени непрямая ветвь разрешится, но побочные эффекты уже присутствуют. Процесс атаки должен убедиться, что он сопоставил один и тот же физический адрес с одним и тем же местоположением в своем виртуальном адресном пространстве, чтобы проверить зондирующий массив с помощью временной атаки. Не уверен, как он это делает, но в окнах AFAIK необходимо отобразить представление объекта раздела на основе файла подкачки, который был открыт жертвой в этом месте. Либо это, либо он будет манипулировать жертвой, чтобы вызвать гаджет призрака с отрицательным TC ebx
значение такое, что ebx+edx+13BE13BDh
= 0
, =1
,..., =255
и как-то время, что позвонить. Это также может быть достигнуто с помощью инъекций APC.