Новогодние подарки, часть вторая: Spectre
Часть первая: Meltdown.
Несмотря на всю мощь уязвимости Meltdown, принесённое этим Новым годом счастье не было бы полным, если бы не вторая часть открытия, не ограничивающаяся процессорами Intel — Spectre.
Если говорить очень-очень коротко, то Spectre — принципиально схожая с Meltdown уязвимость процессоров в том смысле, что она тоже представляет собой аппаратную особенность и эксплуатирует непрямые каналы утечки данных. Spectre сложнее в практической реализации, но зато она не ограничивается процессорами Intel, а распространяется — хоть и с нюансами — на все современные процессоры, имеющие кэш и механизм предсказания переходов. То есть, на все современные процессоры.
Строго говоря, Spectre не является одной уязвимостью — уже на старте заявлены два различных механизма (CVE-2017–5753 и CVE-2017–5715), а авторы отмечают, что может быть ещё и много менее очевидных вариантов.
В основе своей Spectre похожа на Meltdown, так как также базируется на том факте, что в ходе спекулятивного выполнения кода процессор может выполнить инструкции, которые он не стал бы выполнять при условии строго последовательного (неспекулятивного) вычисления, и, хотя в дальнейшем результат их выполнения отбрасывается, его отпечаток остаётся в процессорном кэше и может быть использован.
Любой современный процессор в целях даже не улучшения эффективности, а обеспечения самой возможности спекулятивного выполнения команд имеет Branch Prediction Unit, блок предсказания ветвлений, в задачу которого входит оценка вероятности, с которой выполнение пойдёт по тому или иному пути после какого-либо условия, без предварительного расчёта этого условия. Блок предсказания работает статистически, то есть накапливает данные о выполненных на данный момент похожих ветвлениях, и на их основе прогнозирует исход каждого следующего ветвления.
То есть, например, если код if (a < b), для расчёта которого надо долго и печально загружать a и b, тысячу раз подряд выдал true, то на тысяча первый раз можно с большой уверенность решить, что и сейчас будет true, ещё до того, как из памяти загрузились a и b и собственно произошла проверка.
Что ещё интереснее, процессоры не делают различий между тем, в каких процессах вычисляется это условие. Поэтому, если в процессе malware.exe тысячу раз подряд такой if выдавал true, то процессор будет считать, что первый же похожий if в процессе word.exe также вернёт true.
Это довольно логично, так как в разных программах может встречаться масса очень похожих конструкций, поэтому тренировать блок предсказания на всём потоке данных — эффективнее, чем на одном конкретном процессе.
По сути, только что я описал механизм, с помощью которого программа malware.exe может управлять ходом выполнения программы word.exe, не имея на то ровным счётом никаких формально утверждённых прав.
До 3 января сего года было принято считать, что в этом нет никакой опасности — в конце концов, если что-то пойдёт не так, как ожидает word.exe, процессор в конечном итоге признает ветку спекулятивных вычислений недействительной, сбросит конвейер к исходному состоянию и пересчитает всё заново, на этот раз уже последовательно. Word.exe даже ничего не заметит, кроме небольшой неравномерности в темпе исполнения инструкций процессором.
На этом месте ещё похоже на Meltdown, но дальше начинаются различия. Как мы помним, Meltdown не работает на процессорах, вовремя — до окончания выполнения — проверяющих условия доступа процесса к чужой памяти даже в случае спекулятивного выполнения.
У Spectre такой проблемы не возникает, потому что Spectre не подразумевает прямого доступа к чужой памяти ни в каком виде, даже при спекулятивном исполнении. Вместо этого Spectre делает так, чтобы атакуемый процесс (это может быть как ядро системы, так и другая пользовательская программа) сам выдал сведения о содержании собственной памяти.
Представьте в коде атакуемого процесса такую конструкцию, причём переменная x является следствием какого-то пользовательского ввода, на который мы можем влиять:
if (x < array1_size)
{
y = array2[array1[x]];
}
Теперь мы берём и пишем в своёй программе, эксплуатирующей уязвимость, максимально похожую конструкцию, и выполняем её много-много раз, причём каждый раз честно вычисленное условие выдаёт true, индексы массива совершенно валидны, и вообще всё хорошо. Блок предсказания ветвлений таким образом набирает статистику, говорящую, что эта конструкция всегда вычисляется в true, поэтому, встретив её, можно не ждать окончания вычисления условия, а сразу переходить к содержимому.
А теперь мы передаём в атакуемую программу такие данные, что x вдруг выскакивает куда-то далеко за пределы массива array1. Если бы спекулятивного выполнения не было бы, процессор посчитал бы условие x < array1_size, нашёл его невалидным и перепрыгнул бы дальше. Но оно есть, и блок предсказания выдаёт ему, что x < array1_size почти наверняка будет выполнено, поэтому, пока откуда-то из памяти медленно и печально подсасывается значение array1_size, чтобы действительно выполнить сравнение, процессор начинает выполнять тело этого куска кода.
Важным моментом в атаке, кстати, является пункт «медленно и печально» — если array1_size лежит готовый где-то в кэше, процессор может не заморачиваться со спекулятивным вычислением, а просто быстро посчитать условие. Поэтому array1_size должен быть в ОЗУ, откуда его придётся долго доставать.
Значение x мы выбираем таким, чтобы оно указывало на адрес в области памяти атакуемой программы, который мы хотим прочитать. Допустим, по нему хранится значение k, при этом нам также потребуется, чтобы данное значение уже было загружено в кэш ранее.
Последнего добиться часто не очень сложно — если, например, мы хотим стащить приватный ключ у программы шифрования данных, достаточно предыдущим действием просто совершенно легально попросить программу нам что-нибудь зашифровать, тогда она сама обратится к этому ключу, а процессор, соответственно, затащит его в кэш. Напомню, что ещё одним условием было отсутствие других участвующих в процессе переменных в кэше, но это может быть реализовано простым забиванием кэша всяким мусором от имени атакующего процесса, а то и просто инструкцией принудительного сброса кэша, если такая есть на атакуемом процессоре.
Итак, всё подстроено так, чтобы процессор прочитал array1[x], который будет равен k, что он и делает. Так как k находится в кэше, процессор получает его практически мгновенно, подставляет в качестве индекса в array2 и запрашивает из ОЗУ значение, соответствующее array2[k].
После этой замысловатой цепочки наконец прибывает значение array1_size, процессор вычисляет условие, признаёт его негодным и выбрасывает результаты всех описанных выше вычислений. Исчислил царство и положил ему конец. Везде, кроме кэша.
Что особенно цинично, базовую возможность проведения атаки нам обеспечила проверка индекса массива, необходимая для обеспечения безопасности кода.
Дальше сильно проще не становится, так как нам надо теперь выяснить, что лежит в кэше — и положено оно туда от имени стороннего процесса, то есть, в отличие от Meltdown, напрямую пощупать память мы не можем.
Тем не менее, и тут могут найтись свои методы. Например, если array2 имеет достаточно валидных индексов (не менее k штук), а мы можем снаружи более-менее прямым способом побудить атакуемую программу его почитать, то на индексе k операция чтения выполнится быстрее, чем на других индексах, так как он уже закэширован.
Как нетрудно заметить, способ не отличается простотой и прямотой реализации — однако, при условии атаки на конкретное ПО, известное атакующему и по возможности доступное в исходных кодах в той же версии и на той же системе, на какой предполагается атака, он может быть реализован.
В отличие от Meltdown, этот способ может оставить следы в системы, если атакуемая программа, например, сообщает о входных данных, вызывающих переполнение индекса используемого в атаке массива.
Так же, как и в случае с Meltdown, атакующая программа не требует для себя никаких особенных привилегий, кроме самой возможности запуститься на атакуемой системе. Теоретически, атака может быть произведена даже из JS-скрипта в браузере и других интерпретируемых языков, в которых есть возможности организовать таймер с точностью, пригодной для различения скорости получения переменной из кэша и из ОЗУ.
Это был первый вариант Spectre, который я затрудняюсь как-то коротко назвать. Ко второму же так и просится простое, хорошо знакомое русскому уху название: Гаджеты.
Нет, гаджет в данном случае — это не ваш айфон. Гаджет — это то, что можно использовать для вытаскивания из вашего айфона ваших паролей без вашего ведома.
Гаджет — это последовательность команд в адресном пространстве атакуемой программы, которая может быть использована для атаки. Задачей такой последовательности является организация утечки данных если не напрямую, то через кэш по описанному выше механизму, поэтому последовательность может быть достаточно короткой, а также никак вообще не связанной с какими-либо программными уязвимостями — прямой утечки данных за пределы контролируемой атакуемой программой области памяти, напомню, не происходит.
Важный момент: эта последовательность не создаётся и не вносится атакующим, то есть, опять же, de jure вторжения в атакуемую программу не происходит. Атакующий просто находит нужный ему кусочек кода в теле атакуемой программы или какой-либо из загруженных ей библиотек; более того, в некоторых случаях ему не требуется даже предварительный анализ ПО — непосредственно на атакуемой системе можно попробовать найти нужную последовательность в общеупотребимых системных библиотеках in situ, логично предполагая, что атакуемая программа также эти библиотеки использует.
Исследователи из Google и вовсе использовали функцию BPF — это механизм, существующий в Linux и FreeBSD и позволяющий пользовательскому приложений подцепить к ядру системы свой фильтр, например, для отслеживания I/O-потоков. В данном случае, понятно, совершенно неважно, что этот фильтр будет делать — важно, чтобы в каком-то его месте была нужная нам последовательность команд.
Nota bene: из этого родилась версия, что уязвимость Spectre неприменима при выключенном BPF. Это не так.
Чтобы перевести выполнение атакуемой программы на нужную последовательность, используется подход, похожий на описанную выше тренировку блока предсказания ветвлений — в процессоре есть аналогичный блок предсказания переходов, который пытается угадать, по какому адресу будет совершён переход очередной инструкцией косвенного перехода (мы все помним эти инструкции по Meltdown, но тут они играют другую роль).
Для упрощения работы этот блок не выполняет трансляцию между виртуальными и реальными адресами, а значит, может быть натренирован в адресном пространстве атакующего на определённые действия в адресном пространстве атакуемого.
То есть, если мы знаем, что нужная нам инструкция в атакуемой программе лежит по адресу 123456, а также в этой программе есть регулярно исполняемый косвенный переход. В атакующей программе мы пишем конструкцию, максимально похожую на переход в атакуемой, но при этом всегда выполняющую переход по адресу 123456. В нашем адресном пространстве, конечно, абсолютно валидный и легальный переход. Что именно у нас лежит по адресу 123456, никакого значения не имеет.
Через некоторое время блок предсказания переходов абсолютно уверен, что все переходы такого вида ведут на адрес 123456, поэтому, когда атакуемая программа — с нашей подачи или по своей инициативе — доходит до аналогичного перехода, процессор радостно начинает спекулятивное исполнение инструкций с адреса 123456. Уже в адресном пространстве атакуемой программы.
Через некоторое время настоящий адрес перехода будет вычислен, процессор осознает ошибку и отбросит результаты спекулятивного выполнения, однако, как и во всех прочих случаях применения Meltdown и Spectre, от него останутся следы в кэше.
А что делать со следами в кэше, вы уже знаете.
В целом, по описанию всей этой головоломной процедуры достаточно очевидно, что эксплуатировать Spectre сильно сложнее, чем Meltdown — но, с другой стороны, в той или иной степени ему подвержены если не все, то большинство существующих процессоров.
Кто подвержен?
Можно считать, что все процессоры новее чем Pentium MMX, однако есть нюансы.
- Процессоры Intel подвержены все
- Процессоры ARM подвержены все
- Процессоры AMD, по заявлению компании, «практически не подвержены» атаке через гаджеты, официально называющейся Branch Target Injection или Indirect Branch Poisoning
- Про другие ядра информации нет, но, скорее всего, первому варианту Spectre (Bounds Check Bypass) подвержены все, а второй зависит от реализации в конкретной архитектуре предсказания переходов
Почему именно AMD «практически не подвержены» атаке через перенаправление косвенных переходов, компания не раскрывает. MMU здесь замешан быть не может, так как все запросы осуществляются строго в пределах адресного пространства атакуемой программы. Можно предположить, что у AMD другой механизм предсказания переходов, возможно, отслеживающий, скептически относящийся к идее переноса таких предсказаний между разными процессами. Что характерно, AMD не говорит о полной невозможности атаки, лишь о «почти нулевой вероятности».
При этом AMD точно так же, как Intel и ARM, подвержены первому типу атак Spectre, через обучение блока предсказания ветвлений.
Правда ли, что AMD подвержены атаке Spectre второго типа только на Linux и только при включённом BPF?
Нет.
На Linux с включённым BPF атака была показана в документе Google Project Zero, там же было отмечено, что её не удалось провести на процессоре AMD без BPF — однако, судя по всему, это было вызывано лишь тем, что исследователям не удалось найти в скомпилированном ядре последовательности команд, выбранной ими для атаки. На практике, во-первых, атаки могут проводиться не только против ядра системы, но и против любых исполняющихся в системе программ и используемых ими библиотек, а во-вторых, необходимые последовательности команд могут быть различными. Поэтому, хотя одна конкретная атака могла быть проведена в конкретном случае только через BPF, к общему вопросу уязвимости перед атаками типа Spectre это отношения не имеет.
Производители процессоров обещают простой и быстрый фикс
Во-первых, см. замечание в первой части про отношение к текущим заявлениям производителей.
Во-вторых, Spectre, в отличие от Meltdown, не является какой-то конкретной атакой — это лишь две наиболее очевидные из целого спектра (я в курсе, что «spectre» переводится не так, но уж больно просится) изощрённых атак, использующих возможность целенаправленного обучения процессорных блоков предсказания выполнения программы.
Что мы будем дальше делать, как мы будем дальше жить?
Пока не очень понятно.
Во-первых, вероятно, производители процессоров будут дополнительно тюнинговать их архитектуру с целью исключения или затруднения известных атак. Но результат мы увидим только через два-три года, а кроме того, имеющаяся вольность в алгоритмах процессоров обусловлена стремлением к увеличению их производительности — в обоих случаях со Spectre мы имеем дело с тем, что процессор учится быстрее выполнять один процесс на примере выполнения другого процесса, тем самым фактически позволяя второму процессу контролировать ход выполнения первого.
Глобально решило бы все проблемы более аккуратное обращение с кэшем, например, обнуление результатов спекулятивного выполнения в случае сброса конвейера или сохранение таких результатов в отдельный кэш с переносом их в основной только при успешном выполнении —, но оба варианта также влекут за собой дополнительные накладные расходы.
В данный момент разрабатываются также патчи к компиляторам, обеспечивающие защиту от второй из атак Spectre — на данный момент уже представлены варианты для gcc и llvm, базирующиеся на предложениях Google.
Основываются они на довольно простой вещи: подмене косвенного перехода на возврат из функции. Возврат из функции работает немного иначе, чем косвенный переход, блок предсказания переходов на него не влияет. Фактически, это обман обманщика. Было бы двойным удовольствием, но, как обычно, есть нюансы.
Во-первых, исправление никак не влияет на Spectre первого типа.
Во-вторых, every magic comes with a price. Хотя Google в официльном сообщении аккуратно обходит стороной вопрос о количественном измерении оверхеда, на практике один «защищённый» косвенный переход в среднем утяжеляется в десять раз. Эффект для конкретного приложения зависит от его структуры, языка и компилятора — для ядра Linux он составляет в пределах 2%, для других приложений может быть значительно больше.
В связи с этим на данный момент «официально считается», что для обеспечения «приемлемого» уровня защиты достаточно пересобрать ядро и единичные критичные приложения, а всё остальное и так вряд ли будут отаковать.
В-третьих, никакой общесистемной заплатки для Spectre на данный момент нет и не предвидится — ни от одного из вариантов. Минимальная защита от второго варианта требует полной перекомпиляции ядра системы и, вероятно, в большинстве ОС будет реализована не раньше выхода следующей мажорной версии.
TL: DR
Глобальная ошибка, присутствующая примерно во всех существующих процессорах. Была бы полной жопой, если бы не высокая сложность практической реализации, из-за которой хакеры на неё забьют.
AMD, похоже, наполовину безопаснее прочих, хотя и непонятно, почему.