Pull to refresh

Comments 334

Отличное описание, единственное хотелось бы что-то подобного для spectre, ту уязвимость понять немного сложнее. Я к примеру так и не понял, как ее использовать для чтения данных другого процесса.
Механизм атаки spectre я сам ещё не на столько хорошо понял, чтобы статью писать.
Точнее, про отравление предсказателя переходов понятно, а вот с его эксплуатацией для повышения привелегий — пока не до конца разобрался.
Такой же механизм. Только не читают запрещенный адрес напрямую. (Или как там они его читают в той уязвимости, которую вы перевели.)

Сначала много раз тренируют бренч предиктор, чтобы был заход внутрь if, потом внезапно подают туда адрес, из которого нужно красть значение, и при этом такой, чтобы мы не зашли внутрь if.
Внтури if — чтение памяти.
Бренч предиктор говорит: «скорей всего мы зайдем в if». Это предположение неверно, но пока это неизвестно — и поэтому cpu начинает исполнять то, что внутри if.
В результате выполнения кода, значение из адреса, с которого нельзя читать, попадает к кэш. Вообще, это — исключение. Но потом оказывается, что бренч предиктор ошибся, а раз ошибся, то и исключения нет, и мы как бы и не читали этот адрес.
Исключение отменяется — но остается побочный эффект: данные в кэше.

Фактически, уже это — некорректная работа. Я так понимаю, что если мы прочитаем этот адрес, то получим какое-то значение, и это значение будет не из нашего процесса, что некорректно.

Чтобы узнать, где подтянутое в кэш число, используется атака по времени. Читается все подряд — и замеряется время чтения. То, что было подтянуто в кэш, прочитается гораздо быстрей.

Все остальные бубнопляски в коде — обход других предикторов и оптимизаторов.

Мой AMD phenom x4 B40 уязвим на тесте. Но — тест читает свою же память. То есть, чистоты эксперимента — нет. Кернел спейс читать не пробовал(не знаю его адреса, надо разбираться как узнать, и как потом понять, что прочитал именно его). Может, завтра попробую вечером.

Со второй разновидностью атаки spectre не разбирался, но суть там та же. Неким способом заставить выполниться код, который что-то подтянет в кэш.
Эта часть понятна, потому что суть примерно такая же. А вот дальше, про гаджеты и т.д. — вот в той части не особо разобрался. А вы?
Я испугался этого слова — гаджеты… И не читал ту главу.

Там дальше глава 5, про косвенные переходы. Когда переход делается не по фиксированному адресу, а по адресу, который в регистре, или по адресу который лежит в какой-то ячейке памяти. Можно сделать финт ушами — и «спекулятивно» выполнить не тот код, который должен был бы выполниться. Насколько понимаю — переход по адресу, которого нет в icache занимает ВРЕМЯ и все это время выполняется то, что в этом кэше лежит, только потом это все отменяется, поэтому нам незаметно.
Но этот код, хоть и отменяется, но что-то подягивает в кэш. И дальше как обычно.

Суть одна — тем или иным способом заставляют cpu выполнить код пользователя спекулятивно, причем так, что потом он гарантирванно отменится. А в таком выполнении можно (было до того, как нашли уязвимость) безнаказанно «побегать в труселях по мечети»: все равно никто не узнает.
понял на вашем уровне, но не понятно как происходит чтение не своей памяти.
НЯМС:

там два чтения — одно запрещенное — а другое разрешенное:

temp = array2[array1[x]*512];

х — адрес внутри запрещенной области array1, array2 — разрешенная область.

Спеклятивно это выполняется. Значит, какая-то область array2 подтягивается в кэш.
Дальше мы читаем из array2 подряд, измеряя время чтения. И по времени чтения пытаемся понять, была ли подтянута в кэш соответствующая область.

почему 512 — мне не понятно. Размер кэш-лини — 64 байта, а не 512.
механизм чтения я понял, но вроде как читать ядро нельзя с помощью spectre, а как читать другой процесс мне вообще не понятно. У нас же с ним разные линейные адреса.
Можно получить информацию о том, где в физической памяти находятся страницы чужого процесса, если он не выгружен. Если у ядра вся физическая память смаппирована, то эти страницы можно прочесть.
еще раз spectre не дает читать память ядра, это делает Meltdown только на intel.
Это где такое написано? На сайте лежит pdf, в ней приводится код, и там есть строчка, которую я привел выше.

Насколько понимаю, предполагается, что ядро лежит по адресу array1. Поэтому, array1[x] — чтение из области ядра.

Потом подтягиваются в кэш соответствущая область памяти из array2. Какая именно область подтянется — зависит от значения, которое лежит по array1[x].
возможно я не так понял. Думал что именно в случае spectre не происходит чтения, если не достаточно привилегий.
А в чем бы тогда была его польза? С разрешенных страниц и так читать можно.
тогда непонятно чем 1 баг от другого отличается, только использованием предсказаний переходов?
Во втором «баге», благодаря использованию предсказаний переходов, не происходит исключения при недопустимом доступе.

(Это всё же не баг, а намеренная часть архитектуры.)
я просто думал, различий больше

Некоторые имплантации aarch64 также подвержены.

Если рассуждать так, что 8 бит = 1 байт, то при «разархивировании» получаем => 512 / 8 = 64 байта, и если так идти до конца то получается такая зависимость, приводящая к 1 байт / 8 = 1 бит.
P.s. зависимость притянута за уши, но уж очень хотелось написать. Одна из мыслей, так возможно проще с памятью работать.

Но это требует привилегий для управления кэшом (чтобы вытащить прочитанные данные). А имея такие привилегии и без того можно делов натворить (как минимум DoS). Поэтому уязвимость менее критична — в правильных окружениях она не выполнима.
Можно попытаться косвенно определить значение данных (через спекулятивное исполнение), но это получается долго и нужно, чтобы процесс не прерывался планировщиком.

чтобы вытащить прочитанные данные

Зачем?
Не требует. В цепочку спекулятивных выполнений можно поставить операцию типа address = вытащенный байт * адрес массива. Далее — опредилить, какая строчка массива была вытащена в кэш, например чтением всех строк и измерением времени. Все, значение выдернутого спекулятивно байта известно.
там демонстрируется чтение свой памяти, а вот как читать не свою память я к примеру так и не понял.
Вот и мне непонятно принципиальное отличие Spectre от Meltdown. И там, и там читается значение по произвольному виртуальному адресу — принципиальной разницы между атаками нет. Вот только почему изоляция страниц спасает от Meltdown, а от Spectre — нет?
Meltdown — это чтение защищённых страниц памяти, гонка между отработкой исключения и чтением памяти.
Spectre — отравление или обман предсказателя переходов. Чтение происходит из своего адресного пространства.

Side channel при этом одинаковый — кеш данных.
Чтение происходит из своего адресного пространства.

Так… и ядро тоже в «своем» адресном пространстве, в этом суть проблемы же.
а зачем читать из своего адресного пространства?
Чтобы вылести из js-песочницы в браузере и подампить память всего процесса. А там данные соседних вкладок, недавно введенные пароли, куки и т.п.

Насколько я понял, принцип не отличается от meltdown, только дампить можно то, что и так доступно процессу с т.з. ОС.

Мозилла уже залепила у себя www.mozilla.org/en-US/security/advisories/mfsa2018-01

Хром вроде тоже обещает аналогично сделать (или уже сделал)
Чтение происходит из своего адресного пространства.

К своему адресному пространства мы и так имеем доступ.


И того, что я понял, в случае Spectre в документах идёт отсылка к Berkeley Packet Filter (BPF) — это виртуальная машина, позволяющая в режиме ядра выполнять пользовательский код. Meltdown здесь неприменим, т.к. исключение бросить просто невозможно. А вот выйти за границы массива с помощью Spectre вполне можно.


Но при этом в Windows BPF, насколько я понимаю, нет, поэтому Windows не должна быть подвержена этой атаке.

Эм, нет — документация по Spectre — раздел 5.2 — собственно, Example Implementation on Windows
Spectre — отравление или обман предсказателя переходов. Чтение происходит из своего адресного пространства.

Тут немного чисто языковой путаницы. Да, чтение происходит из своего адресного пространства, только оно «свое» для чужого процесса, а не для атакующего.
Вот только почему изоляция страниц спасает от Meltdown, а от Spectre — нет?

Получается, что через мелтдаун мы можем прочесть данные ядра, и оттуда, либо sensitive данные напрямую, либо уже косвенно — данные чужих процессов. Изоляция всех данных и кода ядра означает, что прочесть ничего не получится.

Spectre же заставляет чужой нам процесс спекулятивно читать данные из своего адресного пространства, которые мы потом подхватим через тайминг атаку на кеш. И этот второй чужой процесс тоже не сможет читать ядро, но зато мы сможем из его памяти вытащить данные так, будто мы в его адресном пространстве и находимся.
про spectre очень интересно, ни как не могу понять как это происходит.
Прочтите оригинал spectreattack.com/spectre.pdf, потому что можно долго Рабиновичем перепевать, все одно для деталей придется читать статью. Да и сам я мог неверно понять.

Кратко (если брать пример под NT из раздела 5.2) — берется код, который зашарен между процессами — dll, ntdll в случае примера. В своем, атакующего процесса, адресном пространстве кусок кода с ветвлением видоизменияется так, чтобы натренировать предсказатель переходов определенным образом. (в атакуемом процессе этот же кусок регулярно атакуемым процессом используется, т.к. это обычный библиотечный код). Предсказатель переходов сохраняет свои предсказания на уровне ЦП, а не процесса, поэтому в том, атакуемом процессе будет происходить то же. Плюс определенным образом сформированные входные параметры — в итоге происходит branch misprediction + чтение данных из пространства атакуемого процесса из-за спекулятивного выполнения, потому что мы натаскали бранч предиктор на заход «вовнутрь». Они потом будут сброшены, потому что переход не произойдет, но в кеше останутся. А из кеша эти данные извелкаются аналогично тому способу, что использован в Meltdown.
Я не понимаю, как мы в другом процессе можем изменить входные параметры.
В этой pdf написано, что мы контролируем используемые регистры в найденном авторами dll кодом — это вообще как?
Да и откуда нам знать, что этот другой процесс вообще исполнит избранный код? В моем представлении для этого нужно изначально воспользоваться какими-нибудь уязвимостями для удаленного исполнения кода, а в этом случае нам spectre уже и не нужен вовсе.
Да и откуда нам знать, что этот другой процесс вообще исполнит избранный код?

Ну, например, запустить эту программу на своей домашней машинке и посмотреть, что там исполняется, а что нет. Учитывая, что они с ntdll игрались, шансов, что код исполнится — много.
Они же использовали Sleep, тут вообще почти что с гарантией.
спасибо, стало понятней. Но для решение достаточно не шарить ntdll, а грузить каждому процессу свой экземпляр или нет? Да это небольшой перерасход оперативной памяти, но это мелочи.
Сам понял, что не поможет. Нам не обязательно использовать библиотеку, можем использовать код чужого приложения.
Я бы не сказал, что это небольшой перерасход. К тому же, просто скопировать маппинг виртуальной памяти при загрузке процесса намного дешевле, чем читать и парсить библиотеку каждый раз. Эта часть — mapped file sections — есть в NT с первых версий, когда она еще не была Windows NT, на нее, кмк, много завязано.
Spectre же заставляет чужой нам процесс спекулятивно читать данные из своего адресного пространства, которые мы потом подхватим через тайминг атаку на кеш.

Ну вот я прочитал оригинал и все равно не понимаю, как можно заставить чужой процесс что-либо прочитать по определённому адресу.


Да, за счёт шаринга DLL, мы можем обмануть branch prediction так, чтобы при определённом системном вызове спекулятивно выполнился кусок кода из адресного пространства процесса-жертвы.


Но для этого нужно, чтобы мы могли управлять регистрами процесса-жертвы перед вызовом системных функций, чтобы читать проивзольные адреса. То есть просто взять и прочитать память чужого процесса не получится.

Да, скорее всего, этот метод не подойдет, чтобы атаковать любой произвольный процесс. Тут надо смотреть конкретно, что за кусок они использовали, можно только предположить, как именно регистры контроллируются. Допустим, это может быть удаленный вызов; обработка данных из сети.
«Чужим процессом» для Spectre может быть само ядро, а функция-читалка — eBFP. Но может быть и другая последовательность в ядре.
olartamonov на гигтаймс разместил статью про spectre, вот ключевая часть второго типа атаки, наиболее интересная:

То есть, если мы знаем, что нужная нам инструкция в атакуемой программе лежит по адресу 123456, а также в этой программе есть регулярно исполняемый косвенный переход. В атакующей программе мы пишем конструкцию, максимально похожую на переход в атакуемой, но при этом всегда выполняющую переход по адресу 123456. В нашем адресном пространстве, конечно, абсолютно валидный и легальный переход. Что именно у нас лежит по адресу 123456, никакого значения не имеет.

Через некоторое время блок предсказания переходов абсолютно уверен, что все переходы такого вида ведут на адрес 123456, поэтому, когда атакуемая программа — с нашей подачи или по своей инициативе — доходит до аналогичного перехода, процессор радостно начинает спекулятивное исполнение инструкций с адреса 123456. Уже в адресном пространстве атакуемой программы.

Через некоторое время настоящий адрес перехода будет вычислен, процессор осознает ошибку и отбросит результаты спекулятивного выполнения, однако, как и во всех прочих случаях применения Meltdown и Spectre, от него останутся следы в кэше.


Под «нужной инструкцией» по адресу 123456 имеется в виду тот самый «гаджет» (кусок кода атакуемой программы, или из какой либо библиотеки, которую она использует), который оставит такие следы в кэше, которые легко отследить из атакующего процесса.
Меня удивляет другое. Один из механизмов атаки — модификация адреса перехода в разделяемой библиотеке. Одна и та же физическая память отображается и в процесс жертвы, и в процессе атакующего. Далее мы меняем адрес перехода в своём адресном пространстве, при этом происходит CoW — изменённый участок памяти маппится на другую область памяти. Но при этом это действие влияет на результат предсказателя переходов в процессе-жертве.
Насколько я понимаю, речь идёт о косвенном переходе, т.е. «модификация адреса перехода» — это не изменение инструкции в памяти, а выполнение её с изменённым значением операнда.
Точно, это я напутал: физически местоположение инструкции одинаково, различие только в адресе операнда, который ещё надо прочитать.
А что удивительного? Предсказатель переходов же не процессо-специфичен. Он процессоро-специфичен. И работает с виртуальными адресами.
В итоге процессор оставляет данные в кэше которые читать было нельзя, фактически произведя чтение, а программа может по косвенным признакам (времени чтения) узнать, что в этом кэше находится. Мне кажется, так немного понятнее, нет?
Нет, данных в кэше не будет. В кэше будет 1 строчка, которая соответствует байту по недоступному адресу. То есть, если по адресу 0xdeadbeef было записано 42, и мы его хотим узнать — то в результате этой атаки в кэше будет строка, которая соответствует индексу 42*4096 в массиве.
Ага, значит ещё сложнее. Значит самих данных не будет, только косвенно будет видно, какое там число — по индексу. Процессор в зависимости от числа в запрещённой для доступа памяти прочитает строку из нашего массива в кэш и уже по строке(которая оказалась в кэше) мы узнаём число. Хитро однако.

Я читал много объяснений того, как это работает. Похоже, половина переводов сделаны один-в-один, и разобраться, что происходит можно только в том случае, когда человек постарался добавить ясности "от себя" или начинает расшифровывать всякие термины, без знания которых сложно разобраться.
Я постараюсь описать то, как я это понял, а вы меня поправьте, если я ошибаюсь.


  1. Процессор может выполнять команды не последовательно, а иногда запускает в параллель исполнение команд, которые в программе расположены позже. Это делается для того, чтобы полностью загрузить свободные вычислительные мощности (конвеер), пока мы ожидаем завершения выполнения других команд (длительное чтение из памяти, например).
  2. Допустимость или недопустимость чтения из памяти работает на аппаратном уровне, и при взятии информации из недопустимого адреса возникает исключение. Важным моментом является тот факт, что команды чтения по запрещенному адресу могут исполняться спекулятивно, с выполнением реального чтения, но проверка на допустимость команды будет произведена позже и, в случае недопустимости этой операции, результат выполнения этой команды будет отброшен и произойдет исключение. В этом месте как раз и получается аппаратный баг — проверка выполняется позже, что скорее всего обусловлено архитектурными особенностями (мы должны помнить, что это не программа, где мы можем за секунду переставить строчку условия, это реальные транзисторы, которые придется перемещать на кристалле, выбирать для них оптимальное расположение и так далее).
  3. Чтение из памяти устроено по-особенному: когда мы читаем один байт, вместе с ним загружается в кэш много больше данных, чем требуется, на случай, если нам нужно будет прочитать следующий байт в памяти по порядку, как в большинстве случаев и происходит. Но если после этого мы начинаем читать по адресу, который не попадает в диапазон адресов, загруженных в кэш, происходит так называемый кэш-промах, и данные в кэш приходится считывать заново, а это сильно дольше, чем если бы данные уже были в кэше.

На основе вышеизложенного, мы можем реализовать следующую схему:


  1. Делаем массив в нашей пользовательской доступной памяти размером char userspace[256 * 4096]. 256 — количество значений которые может принимать байт, 4096 — такое расстояние, на котором гарантировано происходит кэш-промах при последовательном чтении с таким шагом в памяти.
  2. Для i = 0..255
  3. Составляем несколько инструкций так, чтобы сначала была инструкция с чтением из памяти по запрещенному адресу restrictedspace[address], а потом, на основе этих данных, выборка из нашего пользовательского массива на основе значения считанного байта: userspace[restrictedspace[address] * 4096]
  4. Так как нельзя считывать по запрещенному адресу, происходит исключение, но к этому моменту код, который идет далее уже успевает спекулятивно выполниться, и нужная часть нашего пользовательского массива будет загружена в кэш.
  5. Ловим исключение и измеряем время доступа к элементу address[i * 4096], записывая в массив time[i]
  6. Повторяем для всех i (goto 2)
  7. Ищем значение в массиве time, которое сильно отличается в меньшую сторону.
  8. Полученный индекс и будет искомым значением из запрещенной памяти
В первый пункт забыл добавить про спекулятивное выполнение.
В саму ветку, которая читает restrictedspace[address] мы тоже попадаем спекулятивно, поэтому никакого исключения генериться не будет.

В начале кода честно стоит проверка if (address < OUT_OF_BOUNDS), но OUT_OF_BOUNDS задано не константой, а в отдельной области памяти, которая предварительно специально вымывается из кеша.

Это не позволяет процессору мгновенно определить правильную ветку перехода, и в результате он спекулятивно выполняет код, который программа якобы и «не собиралась» исполнять.
Да, я сначала пытался описать это, но по ходу рассуждения не понял, в какой момент происходит неверное предсказание переходов. Можете привести код на ассемблере с этим бранчингом?
Поясните про перемещение транзисторов на кристалле.
от их позиции разве зависит логика работы CPU?
я имею ввиду, что это скорее всего не баг из-за глупости разработчиков, а особенность дизайна. Я имею ввиду, нельзя просто так взять и изменить место проверки этого условия как в обычной программе, придется переносить какие-то блоки, переразводить транзисторы на чипе и так далее. Я понимаю, что сейчас никто уже руками этого не делает, как раз все пишут в виде программы на специальных языках «программирования», но я имею ввиду, что этот перенос может в худшую сторону сказаться на быстродействии процессора, или вообще потребует глубокой переработки архитектуры. Возможно, программный фикс обойдется даже дешевле с точки зрения производительности, или архитектурная переработка ОС может быть даже выгоднее в этом плане.
А Intel скорее всего просто будут сбрасывать кэш при отмене операции, хоть это и не совсем верно, зато безопасно.

Вот я тоже подумал про «сбрасывать кэш при отмене», но не получится ли при этом тогда действовать от противного? Прочитать весь наш массив в кэш, и потом замерять, какая часть была сброшена? 256*4096 = 1 Мб, влезет куда угодно, ну в L3 точно.


В идеале надо не сбрасывать, а восстанавливать состояние, но это, похоже, настолько дорого будет, что все преимущества кэша сойдут на нет. По сути, надо будет держать какой-то теневой кэш для кэша, в общем, не стоит оно того.

Прочитать весь наш массив в кэш, и потом замерять, какая часть была сброшена?

Можете подробнее объяснить? Как мы сможем различить эти случаи?

Различить можно будет, если сбрасывать будут не весь кэш, а только изменённую линейку. Если весь, то да, атака не сработает, как уже ниже пояснили.

Если сбрасывать весь кэш при отмене, а не только последнюю загруженную строку, — то, по-моему, уязвимость будет устранена.
При этом ущерб производительности не колоссально большой, потому что невалидные чтения в действительности выполняются нечасто.
Процентов на 30 просядет производительность.
Меня бы больше устроило, если бы команды rdtsc, rdtscp перевели в разряд привелегированных, или совсем отключили, или повысили гранулярность. Для большинства пользователей это будет ок. Нет часов с хорошей гранулярностью — нет проблемы. Как быстры фикс это — ок.
И вообще, пускай юзают hpet.
Таймер для эксплойта необязателен: один поток пусть в бесконечном цикле инкрементирует счётчик, другой поток пусть сравнивает значения счётчика до обращения к памяти и после.
Как из одного потока в другой передать этот счетчик?
И вообще, что такое «счетчик» и что такое «поток» если мы говорим о микроархитектуре?

Время переключения потоков (которые потоки операционки) — десятки микросекунд. Это гораздо больше времени чтения, пускай даже и из кэша, и само по себе связано с уходом исполнения в ядро.

Если под понятием «поток» вы подразумевали нечто, что выполняется вторым ядром — то как передавать этот счетчик из второго ядра в первое ядро, еще и ненарушив кэш? Для таких вещей есть барьеры памяти, но они влияют на кэш. По сути передача данных между ядрами — через L3 (хотя это от архитектуры может зависеть) Думаю, там не померяешь так просто такой короткий интервал времени…
Как из одного потока в другой передать этот счетчик?

У потоков общая память.


Время переключения потоков (которые потоки операционки) — десятки микросекунд. Это гораздо больше времени чтения, пускай даже и из кэша, и само по себе связано с уходом исполнения в ядро.

А зачем потоки переключать? Они одновременно же работают на разных ядрах.


Если под понятием «поток» вы подразумевали нечто, что выполняется вторым ядром — то как передавать этот счетчик из второго ядра в первое ядро, еще и ненарушив кэш? Для таких вещей есть барьеры памяти, но они влияют на кэш. По сути передача данных между ядрами — через L3 (хотя это от архитектуры может зависеть) Думаю, там не померяешь так просто такой короткий интервал времени…

Во-первых, один атомарный инкремент занимает около 20 тактов. Впрочем, думаю, тут и без атомарщины можно обойтись, чтобы не лочить шину почём зря.


Во-вторых, ничто не мешает N раз провести атаку на одну и ту же ячейку и замерить суммарное время атаки, если точность измерения времени одиночной атаки недостаточна.

Ну так у нас в арсенале имеется команда очистки кэша.
Очищать будем выборочно или все подряд? Если выборочно, то по какому критерию, если все подряд — то как нам помогут множественные итерации?
У потоков общая память.

В том то и дело, что несовсем: мы же на микроархитектурном уровне. Несколько ядер на одну память? Как вы себе это представляете? L0, L1 у каждого ядра — свой. L2 — иногда общий, и то не всегда (Xeon Clovertown E5345). Можете lstopo попробовать (под линуксом). Можно в гугл вбить — и включить картинки, и посмотреть, как вообще бывает. Там иногда такое, что даже не представляю что это.

А теперь представьте себе: из L0 одного ядра нужно число переправить в L0 другого ядра. Это сотни тактов, и этот эффект больше, чем то, что мы хотим измерять. И это надо сделать так, чтобы не повлиять на кэш, потому что момент, в который мы это все меряем — очень «нежный». Любая подтяжка в кэш сама по себе уже повлияет на результат.
И это еще не все. Команды исполняются на конвеере. Там еще есть всякие буферы валидации и прочие оптимизации. То, что придет в соседний проц, если не использовать барьеры памяти, может весьма слабо отражать значение реального счетчика.

Во-первых, один атомарный инкремент

Атомарные операции выполняются с барьерами памяти. Ну ок, можно мои рассуждения выше про конвеер убрать. Но барьеры не отменят остального.

N раз провести атаку

Это приходится делать даже с rdtscp. В том коде они это делают 999 раз — и то не всегда помогает. Другие процессы сильно влияют. Счетчик в другом потоке — гораздо более грубый источник времени, а его получение — гораздо более дорогое, чем тот эффект, который измеряется.
Атомарные операции выполняются с барьерами памяти

В архитектуре x86, насколько я помню, нет необходимости в явном использовании барьеров. И мои эксперименты с атомарными инкрементами показывают, что пересылка занимает явно меньше сотни тактов.


В том коде они это делают 999 раз

В том коде на каждой итерации делается вызов rdtsc. Я бы попробовал переписать код так, чтобы измерялось суммарное время 999 попыток, а не каждой из попыток в отдельности.

Смотря для чего и что надо.
У атомарных счетчиков — барьеры «под капотом».

Где-то у Шипилева была статья про исследование поведения при помощи jcstress, и там было наглядно показано, что можно нарваться на баги. Эти баги неособо частые, но они есть. И я тогда крутил эти тесты на своем амд — эти баги были.

Но тут с вами я согласен: поскольку мы говорим про взлом, то некоторое кол-во некорректных результатов нас устроит. Если 1 раз из 1000 происходит глюк — это не ок для «обычного» софта, для банковского — вообще смерть, а для задач взлома систем — нормально, даже если 1 раз из 1000 все сработает как хочется :).

Так что, пожалуй, что да. Загрублять rdtsc — не способ.
> А теперь представьте себе: из L0 одного ядра нужно число переправить в L0 другого ядра. Это сотни тактов, и этот эффект больше, чем то, что мы хотим измерять

Это не совсем верное рассуждение:

1. Это по любому не сотни циклов.
2. Применяют хитрости для сокращения времени.
3. Загрузку всегда делают в pipeline к операции. Тут конечно большая наука намешана, чтобы соблюсти когерентность данных.

Но когеретность времени тут не соблюдается. Просто доступ из локального кэша много короче следующего уровня, вот схема опроса счетчика и работает.
Нет доказательств. И я не соображу, как это поменять хотя бы примерно. Строго говоря, я тоже не привел доказательств.
Сотни тактов — это я слышал, что из памяти. А если из соседнего L0 — то может и быстрей (там, где L3 общий)
Так ведь промахи кеша случаются нечасто в том числе благодаря тому, что даже когда предсказатель ветвлений условно на каждом 25-ом* повороте выбирает не ту ветку, кеш остаётся нетронутым. Мне кажется вайп в таких случаях сведёт на нет все преимущества кеша.

* perf stat -e L1-dcache-loads,L1-dcache-load-misses,branches,branch-misses /opt/phpstorm-171/bin/phpstorm.sh

Performance counter stats for '/opt/phpstorm-171/bin/phpstorm.sh':

73.192.922.309 L1-dcache-loads
6.005.629.782 L1-dcache-load-misses # 8,21% of all L1-dcache hits
44.366.574.238 branches
1.757.550.166 branch-misses # 3,96% of all branches

41,088747127 seconds time elapsed
При этом ущерб производительности не колоссально большой, потому что невалидные чтения в действительности выполняются нечасто.
Вот же ж, блин, теоретики. Скорость доступа в кэш и в оперативку отличается на два порядка (то есть в сто раз). То есть даже если даже предсказатель будет «промахиваться» в одном случае из сотни (а этого, поверьте, не так-то просто достичь) мы получим проседание производительности на 30-50%.
Речь шла не про предсказания переходов, а про невалидные чтения, приводящие к исключению.
Их намного меньше, чем одно на сто обращений.
Каким образом определять «невалидность»? Пинать MMU при каждом обращении к кешу?
Так же, как она определяется сейчас.
UFO just landed and posted this here
По-хорошему, строки кэша должны иметь флаг «данные загружены во время спекулятивного исполнения», и, либо кэш для спекулятивного исполнения вообще должен быть отдельным, либо этот флаг должен сбрасываться при актуализации спекулятивно исполненных команд. Тогда «мир возможного» будет отграничен от реальности.
Нету отдельного не-спекулятивного исполнения, оно всё спекулятивное. Посмотрите на схему. А вот флаг в кэш, теоретически, можно добавить, только он должен означать «первое использование», и при отмене операции надо выкидывать из кэша только те строки, которые были затронуты этой операцией и у которых есть этот флаг. Но и это, наверняка, не панацея.
Но и это, наверняка, не панацея.
Не панацея — можно заметить как что-то пропало из кеша. Можно для «первого использования» завести отдельный кеш, но тогда можно будет играться с «двойным первым использованием» и т.д. и т.п.

Кажется, что самое разумное решение — одновременно и самое тупое. Современные системы всё равно делят адресное пространство пополам и по старшему биту адреса можно понять — это данные ядра или нет. Ну так давайте сделаем это деление оффициальным и всё.

И не нужно никаких спекуляций, проверка одного бита — это несколько транзисторов, её куда угодно можно засунуть.
кажется, что флаг и проверка — несколько транзисторов, отдельные кэши намного страшнее и дороже. Говорят, кэши на кристалле занимают солидную площадь (больше половины).
Так это уже есть — собственно, именно так защищаются страницы нулевого кольца от третьего. Или вы предлагаете явно ставить барьер при обращении к ядерным страницам, который будет запрещать реордеринг?
Это, собственно, не я предлагаю. Это в статье про Meltdown есть.

Просто все попытки обращения к адресам, у которых старший битик установлен отвергать «с порога» если мы не в режиме супервизора. И всё.

Сейчас же это делается через таблицы страниц. Что, в конечном, счёте, даёт тот же ответ — но требует обращения к большим и сложным структурам данным. А поскольку это медленно — то приходится делать это параллельно с другими вычислениями… со всеми вытекающими…
Это напоминает защиту памяти в старой БЭСМ-6 — один из битов 48 битного слова в памяти показывал, это слово данных или команда. DEP 60х.
А скажите, в 64-битных системах в адресном пространстве тоже только один процесс и ядро присутствует? Или там может быть несколько процессов?
Это как раз в 32-битах можно, теоретически, несколько процессов сегментами развести. В 64-битах ничего этого нет.
Для удобства в память процесса отображается и память ядра, чтобы при входе в ядро не переконфигурировать карту. Просто эта память была закрыта правами доступа.

Также, для удобства ядра, в какую-то область отображается вся физическая RAM, чтобы ядру видеть сразу все процессы и не заморачиваться с подключением нужной страницы, если надо записать в какой-то процесс.

Теперь придётся все эти «удобства» отключать )))
Ну так я, собственно, это и имел ввиду. Есть некая ветка команд, которые выполняются опираясь на предсказание условного перехода, для этих команд в кэш подгружаются затребованные ими данные. Впоследствии же оказывается, что условный переход был предсказан неверно, и действия этих команд аннулируются, а вот подгруженные для них данные остаются в кэше. Что и позволяет потом сделать вывод о том, какие именно данные были задействованы этими «прозрачными» командами. Почему решение с аннулированием соответствующих строк кэша не закрывает данную уязвимость — мне непонятно.
Почему решение с аннулированием соответствующих строк кэша не закрывает данную уязвимость — мне непонятно.
Потому что вам недостаточно просто убрать эту строку из кеша. Нужно ещё вернуть ту, которую вы убрали, чтобы эту положить. Иначе её исчезновение тоже можно заметить. А ещё — всю эту деятельность наблюдает другое ядро, не забывайте, так что наблюдать за всем этим процессом мы можем достаточно пристально.
UFO just landed and posted this here
Мне тут подумалось, для memory mapped io бывает ли такое, чтобы при чтении из ячейки памяти происходили побочные эффекты (типа считали содержимое ячейки — получили значение счетчика и он сбросился в ноль).
Бывает. В лёгкую. И это настолько плохо, что уже во времена Pentium Pro появились MTRR, отключающие спекулятивные чтения для определённых диапазонов памяти.

Так что для атаки это использовать нельзя.
другими словами, от их позиции не зависит логика работы CPU, но вот изменение логики работы может потребовать их перемещения и перекомпоновку блоков. Чипы все еще 2d, так что это может потребовать серьезного изменения архитектуры или может сказаться на производительности

Зачем goto 2? В самом начале сбрасываем кэш специальной командой. Массив userspace[256x4096] в кэше отсутствует. После обработки исключения один из блоков длиной 4096 в этом массиве должен быть загружен в кэш. Измеряем время доступа к элементам с номерами ix4096 для i от 0 до 255. Определяем, при каком i время доступа было минимальным. Найденное i будет равно значению из защищенной области памяти. Массив time для этого не нужен, достаточно помнить минимальное время и значение индекса, в котором оно было получено.

Массив time для этого не нужен

Естественно, просто так проще объясняется алгоритм.


Измеряем время доступа к элементам с номерами ix4096 для i от 0 до 255

А разве кэш-промах и вытаскивание новых данных не испортят нам состояние кэша?

Наш прогон по 255 элементам не должен вытеснить из кэша то, что мы ищем, но параллельные процессы могут это сделать. Поэтому в тестовых примерах из переменной извлекают 1 бит и сравнивают только 2 элемента массива, чтобы понять, что в этом бите было, 0 или 1.

Да, вроде не должен… Но наверное это все же уже оптимизация, в любом случае, нужно экспериментировать. Может зависеть от ОС, процессора и еще кучи факторов вроде запущенных в фоне приложений.
Честно говоря не очень понятно
1) как атакующий может знать какой именно адрес памяти жертвы его интересует — большинство программ используют стековую архитектуру и многие переменные находятся на стеке и вершина стека перемещается с течением времени. Многие объекты так же имеют ограниченный срок жизни между new и delete и адреса создаваемых и удаляемых объектов зачастую почти случайны особенно в многопоточном приложении жертвы. Даже дизассемблирование прогреммы жертвы до атаки не дает реального знания об интересующих адресах в процессе жертвы.
2) атака подразумевает длительное исследование одной ячейки памяти и за это время ячейка просто может поменять свое значение в многопроцессорной системе.
То есть метод абсолютно не подходит, чтобы сделать снимок памяти процесса жертвы.

Иными словами, если с помощью атаки «прочитал» байт из памяти жертвы, то насколько атакующий может быть уверен, что это именно тот байт, что его интересует?

Мне кажется метод атаки имеет академический интерес, но вот попроси любого из нас провести реальную атаку даже в лабораторных условиях — и мы не сможем и я не смогу.
Мне кажется метод атаки имеет академический интерес, но вот попроси любого из нас провести реальную атаку даже в лабораторных условиях — и мы не сможем и я не смогу.
Вы не сможете, а авторы соотвествующих статей смогли. Пароли из менеджера паролей вороются на раз. Просто потому что они не могут жить на стеке (они ж глобальные и должны там храниться годами) и менять их каждые 5 секунд тоже вроде как незачем.

И главное, что атака — ничего не ломает! Не получилось сейчас, попробуем через час… У многих браузеры сутками не закрываются!
Пароли в нормальных менеджерах лежат в базе данных в
зашифрованном виде и в память они попадают временно в момент когда их запрашивают (GUI/API)
Вероятность вытащить такой атакой пароль от важного ресурса призрачно мала.
такому удачливому хакеру нужно срочно бежать покупать билет «русском лото» — джекпот гарантирован )

это при условии, что вы каждый раз вводите пароль. А не храните сессию
Предлагаю провести эксперимент:
1. Откройте свой менеджер паролей.
2. Снимите дамп памяти процесса.
3. Найдите в дампе свои пароли.
4. Назовите этот менеджер и его версию.
3. Найдите в дампе свои пароли.
Кто вам сказал, что они там «открытым текстом» будут храниться? Там где-то будет мастер-пароль и зашифрованная база со всеми остальными.

Но если менеджер паролей может их расшифровать (а он может, раз не спрашивает мастер-пароль повтороно), то и мы сможем… если достаточно долго посидим в отладчике предварительно…
в память они попадают временно в момент когда их запрашивают (GUI/API)

А что мешает их каждый раз во время атаки запрашивать?
Пароли в нормальных менеджерах лежат в базе данных в
зашифрованном виде и в память они попадают временно в момент когда их запрашивают (GUI/API)
Если у вас менеджер спрашивает мастер-пароль каждый раз при необходимости показать какой-либо другой пароль — то да. Но большинство пользователей такого уровня параноидальности не выдерживают.

И «хранилище паролей» остаётся открытым когда минутами, а когда и часами. В каком-нибудь keepass'е по умолчанию сессия вообще закрывается только когда пользователь сам об этом попросит.
1) Брутфорс никто не отменял. В случае Meltdown мы можем просто сдампить большой кусок памяти, а затем искать в ней какие-нибудь интересные строчки.

2) Что-то поменяется, что-то нет. Здесь и не нужна 100% удача.

А вот Spectre действительно имеет больше академический интерес — слишком много условий должно выполниться, чтобы было возможно прочитать память чужого процесса.
А вот это уже вопрос номер два. Я думаю, что специалисты по взлому, имея исходный код chromium смогут в два счета понять, что им нужно, чтобы считать именно нужную область памяти. А вообще, да, это задача творческая и нетривиальная, но сама по себе уязвимость — это возможность начать работу по изучению. Я думаю, что появление реальных программ, способных узнать ваши пароли и номер банковской карты — это вопрос времени.

А мне может кажется или ваш цикл на шаге 2 нужно поставить после 4-го пункта? Ведь смысл от цикла — измерить время доступа к данным массива, чтобы получить значение байта по "невалидному адресу", а измеряем время доступа мы только после того, как поймаем исключение. На шаге 2 нужен другой цикл, ИМХО. Цикл, который будет читать последовательно байты из памяти ядра, чтобы, собственно, и получить интересующий нас пароль к почте Клинтон.
P.S. А так за объяснение плюс. Стало намного понятнее все.

В пункте 5 у меня опечатка:


Ловим исключение и измеряем время доступа к элементу address userspace[i * 4096], записывая в массив time[i]

Цикл стоит в правильном месте, поскольку сложно гарантировать, что за 256 итераций наша искомая кэш-линия не вытеснится из кэша. Например, ОС решит выделить квант времени другому процессу, который испортит весь кэш. В данном случае, приведен медленный, но более стабильный вариант.

Ну и как я понимаю, для экономии на переключении страниц памяти все системы так делают — проецируют память ядра в пользовательскую.
Не все. У микроядерных — честный IPC.
Нет у всех. Часть памяти ядра должна отображаться всегда и во все процессы. При возникновении прерываний, исключений, вызове sysenter (честный IPC) происходит переход на адрес который должен находится в памяти.
происходит переход на адрес который должен находится в памяти.

Да, но это может быть микроскопический трамплин.
Да, патч для Linux — KAISER так и делает. Просто необходимость проецирования части памяти ядра определяется архитектурой не ОС, а процессора. Защищенный режим у intel предполагал невозможность чтения памяти ядра прикладным ПО и «трамплины» не делали. Это лишний, сложный и медленный код, дублирующий функции железа. А теперь выяснилось, что из-за излишних оптимизаций этот код необходим.
Сисколлы любой процесс может вызывать, к памяти и карте ее распределения это не имеет никакого отношения. Проецируют память только по одной причине — чтобы часть кода ядра попала в кэш и была там по возможности как можно дольше, а лучше — всегда, но все это на усмотрение процессора. В любом случае ОС проводит значительное время(1-10%) в режиме ядра и выполняет шаблонные действия, и вот выполнение этой части можно ускорить — держать горячие части кеше.
Меньше возни с таблицами страниц — это побочный эффект, главное — ядерный код работает быстрее.
Проецируют память только по одной причине — чтобы часть кода ядра попала в кэш и была там по возможности как можно дольше, а лучше — всегда, но все это на усмотрение процессора

При перегрузке CR3 сбрасывается TLB.
У каждой задачи своя таблица страниц, но если дописать в нее маппинг на страницы ядра, то процессор будет(очень вероятно) держать эти страницы в кеше.
Ну, это зависит от соотношения интенсивности работы с памятью ядра и прикладной задачи.
Как это зависит от того, есть «дописывание» или нет?
Юзерспейс код работает в 95% времени, остальные 5% — ядро(цифры от балды, но суть — у юзерспейса огромный перевес). У ядра просто нет шансов попасть в кеш(мы не tlb, а обычный L1-L2-L3), а очень хочется, вот настойчивое «дописывание» и помогает процу понять, что эти страницы желательно держать в кеше.
Ужас какой. Вы тут развели целую теорию вместо того, что вам уже сказали: при перегрузке CR3 сбрасывается TLB. А наличие одного и того же фиксированного маппинга во всех процессах позволяет CR3 при переходе в ядро и возврате не сбрасывать. Вот и всё.

А что с этого можно ещё каких-нибудь «плюшек» получить — это мелочи…
CR3 сбрасывается каждый рад при переключении задачи, но сейчас есть возможности не сбрасывать весь TLB — и делается это не мэппингом страниц ядра.
Этот огород городили не ради 4кб TLB. Мэппинг — стародавний и работающий на всех платформах способ пропихнуть страницы с кодом и данными ядра в кеш процессора. Таггинг изобретут сильно позже, а пока это работает.
CR3 сбрасывается каждый рад при переключении задачи

Но не при прогулке в ядро и обратно в пределах одной задачи. Для чего и нужно маппирование в нынешнем виде, иначе CR3 придется переключать дважды, сбрасывая TLB при каждом syscall'е и при каждом возврате из него.
CR3 переключают дважды, если pti включен.

О чем я и сказал — недешево это.
95-5, вот именно. При чем тут дописывание? Что, от дописывания страницы в кеш попадут что ли?
Все ядро — 1-2 мб, самые нужные структуры данных еще 10мб, еще 100мб буферов. В реальности это выглядит как минус 1-2гб памяти прямо со старта Винды(и любой другой ОСи), но само ядрышко и горячие данные компактны, локальны и кешфрендли, поэтому с легкостью залетают в кеш.
Вы не понимаете уточняющих вопросов и говорите совершенно отвлеченные вещи.
Есть архитектуры, где проверка доступа происходит ДО запуска чтения. MIPS например (Байкал-T1). И в AMD похоже тоже.
а почему исключение отрабатывает не сразу? Насколько понимаю, уже это — уязвимость.
На сколько я понимаю, корректность доступа проверяется в самом конце, на стадии retirement. И это имеет смысл, если большая часть инструкций валидна — меньше будет тормозить пайплайн. Но это уже мои домыслы, насколько глубоко документацию на процессоры я не копал.
Логично, что они оптимизируют лучший случай — в 99.9% случаев чтение будет валидным, а 0.01% чтений зловреда или просто некорректного кода можно пренебречь — там performance penalty неважны.
Сразу прошу прощения за нубские вопросы, т.к. я не силен в низкоуровневом программировании. Можно ли код, эксплуатирующий такие уязвимости, засечь еще на этапе загрузки? Должен же он иметь некие паттерны? Если да, то может отказывать такому коду в загрузке и исполнении будет дешевле, чем выключать отображение страниц памяти ядра в адресное пространство процесса?
вон же код, прямо в статье. взять и запретить загружать такой код и всех делов
Код атаки — всего четыре инструкции. Уязвимых комбинаций инструкций очень много. Они очень часто встречаются в дикой природе (в разных вариациях).
парсер порезал irony… оно конечно там было. :)
Верификатор загружаемого кода может проверять, что все обращения в память происходят либо к статически выделенной памяти, либо к стеку, либо к тому, что вернул new/malloc. А там, где есть сомнения, ставить breakpoint, который сломает конвейер (а лучше вообще не давать запускать).
Поздравляю, вы изобрели JVM.
Верификатор загружаемого кода может проверять, что все обращения в память происходят либо к статически выделенной памяти, либо к стеку, либо к тому, что вернул new/malloc.
πздец, извините за выражение. Выходят статьи, диссертации, разные инструменты и прочее, которые нужны для того, чтобы это свойство, с некоторой вероятностью, можно было обеспечить.

А тут раз: щелчок пальцев — и верификатор у нас в кармане.

А там, где есть сомнения, ставить breakpoint, который сломает конвейер (а лучше вообще не давать запускать).
Нечто подобное, примерно, и предлагается. Но там приходится почти везде это делать, так как магического валидатора у нас нету…

Теория алгоритмов утверждает, что создание подобного верификатора попросту невозможно.

Это не совсем правда. Мы не можем точно понять — обладает программа таким поведением или нет. Но можно «пограничные случаи» обьявить «подозрительными» и тоже выкинуть.

Проблема в том, что теоретически, это даст нам безопасность, а практически — страшную головную боль и почти все библиотеки окажется невозможно использовать. Останутся разве что олдскульные программы типа TeX'а, которые память выделяют в самом начале работы, а освобождают — по завершении…
Верификатор загружаемого кода может проверять, что все обращения в память происходят либо к статически выделенной памяти, либо к стеку, либо к тому, что вернул new/malloc
Это не защищает от Spectre.

Там же код
if (index < array_size) { обращение к array[index]; }

Проблема в спекулятивном выполнении, когда процессор зашёл внутрь if при слишком большом index, а не должен был. Верификатор тут посчитает, что такое чтение закрыто if-ом.

Если же верификатор будет заходить во все false-ветки и пытаться выполнять код, который не должен выполняться, у него быстро крыша съедет от ложных срабатываний.
Паттерны есть, но они «размыты», как бинарный боеприпас. Кусочек, которые «начищает предсказатель» там, «читатель тени» тут, какая-то логика, которая всё объединяет — ещё где-то.

Каждый кусочек — вполне «бытовой», встречающийся в обычных программах. А как понять что в рантайме вон тот, тот и вот ещё этот участки соберутся и получится эксплоит — неясно совершенно.

Потому и паника такая, что уязвимость, сама по себе, не особо кошмарная, но вот то, что проведённая через неё атака не оставляет никаких следов — это страшно…

Вот такой кусок кода даст примерно такой же ассемблерный код:


struct foo {
    char flags;
    char padding[4095];
};

struct foo array[256];
unsugned char* index_ptr;

do {
    char flags = array[*index_ptr];
} while (flags == 0);

И этот код вполне себе обычный.

Я правильно понимаю, что с этими уязвимостями ничего толком не запустить? Мы можем только попытаться угадать, что хотел предсказать процессор при запросе данных из определенного места памяти?

Ну Интел сказал же, что это не RCE.
Но запуск и не требуется — просто прочитать из памяти что-нибудь вкусненькое тоже хорошо.
Меня вот лично очень пугают сообщения, что чуть ли не через JS можно будет содержимое оперативной памяти читать. Зашёл на сайт с нехорошим скриптом — и пароли из KeePass утекли. Вот где ужас-то.

Вы пробовали снимать дамп памяти процесса KeePass — можете найти там свои пароли?
Вот-вот. Плюс нам надо запуститься в системе как минимум от юзера. Вычислить адрес приложения. Рассчитать где у него хранятся пароли. С вероятностью в N процентов попытаться их вычитать, ведь в кеше может быть не только то, что нам нужно. И на каждый тик процессора нужно создать как минимум 256 своих тиков, что жутко подвесит систему. Ну так себе уязвимость. На сервере может и не заметит никто, но рабочую машину я патчить не собираюсь.
Все что вы написали не верно.
1. Вероятность прочитать правильно 99.97% (0.9997 если говорить в ТВ)
2. То что мы читаем не обязано быть в кэше, кэш используется для извлечения данных, читать можем что угодно.
3. Читать можно файловый кэш к примеру. Вроде как для js убрали эти примочки, но к примеру запуск flash может быть очень неприятный
читать можем что угодно.
3. Читать можно файловый кэш к примеру.

Вы могли-бы пояснить как файловый кеш относится ко внутреннему процессорному кешу предсказаний переходов? Я, как понимаю, чтобы его (файловый кеш) считать — нужно его запросить. Для этого нужно иметь доступ к системе. А для этого нужно уже быть вирусом в системе. Тогда ЭТА уязвимость ну не совсем нужна. Точнее совсем не нужна. Если мы в системе и может запросить на чтение файл с достаточными правами — мы можем и так его считать. Может я чего не понимаю — поясните.
Мы можем читать любую память, включая память ядра. Файловый кэш хранится в памяти ядра. Собственно все. Естественно, если файла нету в кэше, то его не удастся прочитать.
Т.е. чтобы считать хранилище паролей windows или chrome мы должны торчать в системе. Тратить 256 тиков на каждый процессорный тик только на чтение. Нам еще нужно обработать весь этот поток данных, чтобы понять, что мы читаем. Собрать все данные в один массив, система-же многозадачная и данные там будут не только ядра. Расшифровать его. Это вообще реально?
все реально, легко снимается полный дамп памяти. А дальше уже можно много вещей делать.
Как у вас получится снять консистентный дамп памяти и успешно разобрать что/где лежит в этих XX гигах?
Приложения работающие с секретными данными
явно знают и используют функции вроде
msdn.microsoft.com/en-us/library/aa366877(v=vs.85).aspx
и не хранят открытые ключи в памяти «вечно»

И потом совершенно незаметно эти XX гигов куда-то сохраняются и отсылаются? Да это даже не троянский слон, это троянский танкер какой-то.
Это даже не танкер — это ходячий замок хаула. Мне не понятно какая должна быть параноика у антивируса, чтобы не заметить такое.
UFO just landed and posted this here
Да вообще не у всех стоит, но есть-же встроенный WinDefender. Практически не отключаемый. Проще-же было обновить его, чем городить патчи с 30% просадкой мощности.
А это уязвимость можно закрыть на уровне антивируса?
Первая, вроде как закрывается виагрой, но сам не пробовал — советовать не буду. Если вы про ту, что обсуждается в статье, то ИМХО можно закрыть предпосылки к её эксплуатации. Т.е. не давать стороннему софту пролезть на комп вообще.
JS — это софт исполняемый на стороне клиента, к примеру. Как вы собираетесь валидные, с первое взгляда, инструкции запрещать?
Валидность можно проверять верификацией кода перед запуском, как это делает JVM с байткодом. Что-то валидное может проверку не пройти, но это не очень большая проблема.
Вопрос остается только в том на сколько нечто невалидное отличается от валидного… Ну то есть если процент ложных отбраковок будет в районе долей процента — это сойдет, а если это будут единицы процентов?
Да даже если вы сможете придумать валидацию, в чем я сомневаюсь.
Сейчас, если зайти на какой-нибудь развесистый сайт, то и на ноутбуке браузер начнет замирать. А представьте, что вы еще начнете делать описанную валидацию…
Будет замирать еще дольше )
UFO just landed and posted this here
Зато любой обратит внимание на чудовищного объема исходящий траффик и/или на активное использование диска. Естественно, говорю о домашних пользователях, с серверами посложнее.
Отсутствие активности мышки — вполне себе повод считать что юзера рядом нет…
Ну и именно отправка огромных массивов — не столь уж обязательна.
А если скопировать с памяти ядра токен который даёт нужные права приложению? Критических и весьма ценных данных в системе может быть не так уж и много, если знать где их взять — времени у троянца может быть просто масса, и если даже он будет угадывать по одному байту на системный тик с довольно высокой вероятностью он может выудить все необходимые пароли. А если в системе есть какой-то ключик который предоставит полный доступ вредоносному коду… то выуживать таким образом гигабайты не нужно — считываем ключ, пользуемся им для доступа к нужным нам гигабайтам данных — пара милисекунд после вторжения и система скомпрометирована.
Ещё раз, чтобы это сделать вы должны быть в системе как минимум от юзера. на сервере может и прокатит. На личной машине патч по сути бесполезен. Атаку понимающему человеку будет видно сразу. У остальных и так логины сопрут через фейковые сайты.
Простой пример: Представим что JS может спокойно читать память с этой уязвимостью
Итак для облегчение у нас есть браузер с 1 процессом на всё
Открыто 2 страницы 1 с JS другая с <паролями> всего-то нужно хацкеру узнать адрес в памяти для нужной страницы браузера и найти место где хранятся пароли – и очень хорошо если пароли хранятся в чистом виде

Но в реальности на 1 страницу браузера 1 процесс — надо найди нужный процесс
В нужном процессе — найти нужное место в памяти где хранятся пароли

Ребята да вас развоят – легче сломать какой-то саб домен пентагона через открытый порт – чем вытянут пароль реального процесса с кеш памяти процессора

Это умышленное устаревание техники – чтоб лучше покупали новые процы
Такой же развод как и силиконовая лотерея
(кто не знает реальный процент брака там меньше 1 %
и даже если б он реально был больше, То топовыйх процов (без брака)
было б больше чем бракованных – но по факту продаж в разы наоборот – странно правда.
(они могут как блочить в самом проце – так и спецом под шумок браковать если будет масс расследование, это литография – новая схема как два пальца об асфальт — вы ничего не докажите XD)
(прям как в Апл с батареёй – типо они тут ни причем и сделаи ради пользователей
Еслиб делали ради них то былаб галочка в настройках, хотя даже так пользователям пох
Ибо они бегают от зарядки к зарядке весь день – и телефона макс хватает на 24 часа, а часто и на 12,
а так это временной триггер для включение устаревания после N дней работы)
(p.s вполне возможно что уже совсем скоро тот же “временной триггер устаревания” бeдет и в процах, если ещё его нет в последних моделях)
Хочется взять и подарить учебник по русскому языку
Ну, спасибо хоть не назвали “больным на голову параноиком”,
а только “неграмотным школьником” ;)

К примеру была такая уязвимость как Row hammer, пару лет назад
И большие компании скопом взяли и «положили» на неё,
Типо будет новый hardware – там пофиксим, а делать апдеты нету смысла
– типо она не опасна, а тут на тебе — прошло 3 года и она внезапно стала опасна В новой форме
(и пофиксить её решили патчем всех ОС что уменьшает производительность)

p.s А ничё что JS и так может натворить такое, что на голову не налазит – и без всяких ваших новомодных уязвимостей
– и всем опять откровенно на это “положить”
Видели — что патч творит с серверами?
www.epicgames.com/fortnite/forums/news/announcements/132642-epic-services-stability-update
я прикидывал на 50% падение производительности (в 2 раза),
а в реальности бывает и на 70% — в 3 раза (на графике видно как с 10 выросло до 27)
в этом нет ничего необычного
на сервер где 32 ядра может работать под 100+ виртуалок
каждая с 50+ сервисами/процессами
– и при каждом переключении между ними сбрасывается “CPU оптимизация ”

p.s разочаровал Linux что также как и Microsoft бездумно все пропатчил,
а то что люди верят во всякую ересь тут и удивляется нечему
(Как в то — чтоб сделать процы быстрей надо уменьшить тех процесс,
хотя в реале надо уменьшить TDP, а TDP напрямую зависит от кривого дизайна,
когда 30 процов стоят в притык друг к другу и нереально греют друг друга,
и чёт я сомневаюсь что это было сделано случайно – они ж не полный раки,
и таких рачных мелочей намного больше)

pp.s – к сожалению дальше больше, это один из первых таких патчей…
(может такое было и раньше, но я не помню)
Вы немножко не понимаете, чуть больше, чем совсем. Дело тут не в процессах. Нельзя просто взять и считать данные отдельного процесса. Можно попытаться отследить когда на конкретном ядре процессора выполняется код конкретного процесса и попытаться считать его данные кеша. С не 100% Вероятностью, и потом попробовать извлечь из этих данных что-то. На что потребуется, ИМХО, мощность на порядки больная, чем атакуемого ПК.
Нельзя просто взять и считать данные отдельного процесса.
Почему нельзя-то? В структурах данных ядра написано — где чего хранится. У нас есть доступ ко всей памяти. Заходим и читаем.

Из кеша ничего не читается, но можно сделать полный дамп памяти ядра. А потом применить volatility.

с javascript — не очень ясно, как обратиться из js к интересуемым адресам. Вроде бы, js такое не позволяет, это же безопасный язык.
Лишний повод не поддерживать эту технологию.
Уже не впервый раз встречаюстя с этим мнением.
Нет, через JS нельзя читать память других процессов используя Spectre. Но можно прочитать память своего процесса, например, браузера. Что тоже неприятно, но не настолько фатально.
Честно говоря, я не особо понял, зачем так делать — я бы просто к rax прибавил единичку сразу после чтения, да и всё


0x00 станет 0x01, а что будет с 0xFF? Такие байты станут нулями. Т.е. проблема не исчезнет, только может немножко ускорится чтение, если нулей больше, чем 0xff.
Но зато похоже будет 2 операции, задействующие ALU и я не уверен что в этом случае все эти команды сможет спекулятивно выполнить prefetch. Надо проверять.
0xff станет 0x100, так что там всё хорошо. Прибавлять единичку я предлагают уже в rax, там 64-битное значение хранится.
Так делается потому, что если возвращается ноль — есть вероятность что успело отработать исключение и запретить доступ в память — поэтому попытка предпринимается еще раз
А вот интересное дело: в первоначально документе сказано, что хотя PoC не удалось заставить работать на AMD и ARM, скорее всего не получилось из-за плохой оптимизации самого PoC, т.к. ассемблерный код из статьи отработал и там с ожидаемым результатом.
Пункт 2 плохо объяснён, хотя именно в нём и кроется разница между intel и amd.
Читаем интересную нам переменную из адресного пространства ядра, это вызовет исключение, но оно обработается не сразу.


На intel'ах проверка прав производится после чтения, а на amd — до. Таким образом, у intel'а получается спекулятивное чтение (прогревающее кеш), а у amd — спекулятивный segmentation fault.
Осталось прикрутить термометр к регистру, хранящему Segmentation Fault, чтобы определить был ли он задействован.
Если это так, как написал amarao, этот регистр по-любому будет задействован… Неудачная шутка…
С другой стороны, это может объяснить почему на разных семействах процессоров Intel уязвимость есть, а у AMD — нет. Независимо от физического расположения регистров — кэшей на кристалле. Логика отработки спекулятивного исполнения разная.
Как это у амд нет?

интел не устойчив к обеим типам
амд не устойчив только к spectre

Но почему-то народное внимание акцентировано на интелах и meltdown, а амд в головах людей «надежны». Думаю, не последнюю роль тут сыграли маркетологи амд.
На самом деле, это означает, что уязвимость есть везде. Я еще не разбирался с meltdown, но навскидку уязвимости весьма похожи. По сути, это 1 тип уязвимости, просто мы meltdown-это вид в фас, а spectre — вид в профиль. И основная уязвимость — та, которой подвержены обе фирмы, то есть spectre.
Похоже, что уязвимость присутствует не только у процессоров Intel и AMD, но и у многих процессоров с архитектурой ARM.
Без понятия. Возможно, то, что написано по ссылке, соответствует реальному положению дел. Но на моем phenom b40 уязвимость есть.

Да и то, что они написали — «Resolved by software / OS updates to be made available by system vendors and manufacturers» — это же отмазка! Хочется же, чтобы и баз был закрыт — и производительность не падала. А патчи существенно снижают производительность (уже есть статья про это), повышают энергопотребление и тепловыделение.
> Но почему-то народное внимание акцентировано на интелах и meltdown, а амд в головах людей «надежны».

Meltdown легко сделать, даже в JS. И он не работает на AMD.

Spectre значительно труднее сделать, по крайней мере Гуглу пришлось воспользоваться eBPF, который по умолчанию выключен.
Разве в js можно вызвать сегфолт? Только Spectre возможно через js эксплуатировать.
Тогда бы не работал toy example, а он работает. В статье пишут, что их PoC код просто не успевает по какой-то причине.
Кстати, тут должно стать понятно, как работает гипертрединг

Пожалуйста, либо «гиперпоточность», либо «хайпертрединг», а не то смешенье французского с нижегородским.
Хайпертрейдинг — это торговля Биткоинами? От слова «хайп» и «трейдинг»?
Сходить в оперативную память стоит больше 100 процессорных тактов

Это все еще так? Вроде последнее поколение оперативной памяти всего лишь
раза в 1.5 уступает в частоте написанной на упаковке?

На упаковке памяти? Или на упаковке процессора?
Частота работы и задержка никак не связаны друг с другом. Память не отдает запрошенное значение за один такт своей работы.
И да, задержки памяти, измеренные в секундах, насколько я понимаю, уже давно почти не меняются. Выросла частота в N раз — выросли и задержки в ~N раз. Соответственно, для процессора они не поменялись.
Я – Мелтдаун. Я не баг ваш.
Я происхожу от вас и существую в вашем проце.
Да не нарушишь ты принципа причинности в моем потоке выполнения. А не то.
Зачем ругаися, насяльника? Мы патш сделать, теперь работать медлена но шэсна.
image
Насколько я понял, для успешной атаки необходимо иметь машину с хакнутым вектором исключений. Иначе вирус просто уйдёт в аут, прочитав лишь несколько байт. Риторический вопрос: зачем нам тогда Meltdown, если мы уже взломали всё на свете?
Нет, и в этом смысл. Результат спекулятивного чтения процессор отвергает, но остаются сайд-эффекты, которыми и пользуются в этой атаке.
exce1 имеет в виду: результатом чтения, к моменту retire, будет исключение доступа к памяти, и эксплойту потребуется, чтобы это исключение его не убивало.
Никаких «хакнутых векторов» и никаких особых привилегий для этого не нужно (в каком-нибудь C++ достаточно обернуть чтение в try{}catch или выставить предварительно signal(SIGSEGV)), но как это эксплойтить из ЯВУ наподобие JS, мне непонятно.
На самом деле не надо ничего. Т.к. исключение в коде, который не должен был выполняться, (хоть и выполнился спекулятивно), то и исключение будет отброшено вместе с результатом чтения.
Посмотрите псевдокод в статье: там безусловное чтение по недоступному адресу.
В том-то и дело. Исключение НЕ БУДЕТ отброшено. В опубликованном proof-of-concept коде чётко прописан обработчик на signal(SIGSEGV)), а также активное использование инструкций RTDSCP:
github.com/paboldin/meltdown-exploit
Очевидно, что ни через какой браузер выполнить такое невозможно.
Ага, значит со вторым концептом перепутал, где предсказатель перехода обманывается.
В другой статье описывается подробнее — память разделяется между процессами, один и них падает, второй проверяет попадание в кеш.
По идее, можно было бы при падении процесса пробегаться по всей разделяемой им с кем-нибудь памяти до переключения на процесс, который мог бы ее прочитать. Но это усложнит планировщик процессов.
А как узнать пропатчили меня уже или нет? (Linux). Или это произойдет при очередном обновлении ядра?
Говорят, что в линуксе их можно отключить параметр nopti в kernel command-line parameters.
Смотрел вот тут и никакой nopti не нашел. Нашел схожий по звучанию параметр «nopku», который
[X86] Disable Memory Protection Keys CPU feature found in some Intel CPUs.
Как-то так.
$ grep ISOLATION config
CONFIG_MEMORY_ISOLATION=y
CONFIG_PAGE_TABLE_ISOLATION=y
Еще можно попробовать:

$ grep insecure /proc/cpuinfo
Не показательно относительно kpti fix
$ grep ISOLATION config
CONFIG_MEMORY_ISOLATION=y
CONFIG_PAGE_TABLE_ISOLATION=y
$ grep insecure /proc/cpuinfo
bugs: cpu_insecure

На opennet видел такую инструкцию:
zcat /proc/config.gz | grep PAGE_TABLE_ISOLATION
CONFIG_PAGE_TABLE_ISOLATION=y
Не все дистрибутивы выкладывают текущий конфиг ядра в /proc/
Я написал общий случай подставьте файл и выберите grep или zgrep

А если в п.2 сделать не
char tmp = *kernel_space_ptr ;

а
char tmp = (*kernel_space_ptr >> k) & 1;

и пытаться восстановить блок памяти не по байтам, а по битам? Или такое не пролезет через конвеер? Должно быть быстрее на порядок.
На самом деле именно так и делают, раздел 5.2 оригинальной статьи.
Мне вот интересно, какие пароли злоумышленник может заполучить через уязвимости Meltdown и Spectre? Речь только о сохранённых пользователем паролях в браузерах, или под угрозой также пароль на вход в систему, от BitLocker, от .rar архива, в конце концов?
Есть ли изменения в производительности для Ryzen (до и после обновления, OS Win10)? Может кто тесты находил.
Глупый вопрос. Вот нам удалось записать значение байта в tmp и… всё. Почему мы не используем непосредственно его, а ищем косвенными путями? Через некоторое время после чтения ядра произойдёт исключение, но процесс всё равно продолжает работать после этого, так почему бы не использовать tmp напрямую?

Потому что процессор "случайно" выполнил это, ведь на самом то деле исключение уже произошло и весь результат работы надо выкинуть. И он выкидывает.


Вы же, когда на код смотрите, сами видите, что исключение будет выкинуто на строке


char tmp = *kernel_space_ptr;

и, с точки зрения даже ассемблерного кода, не говоря уж про С, никакой результат в tmp не попадёт.

Исключение произойдёт вместо записи прочитанного значения в tmp.
это спрятанный tmp, ветка неправильная(хоть и выполненная) и процессор его нам не отдаст.
А если прибивать процесс сразу после попытки чтения памяти ядра, эксплойта уже не будет?
Немного усложнится обработчик прерывания, но серьезного снижения производительности не будет.
Я не представляю, зачем честным приложениям может потребоваться пробовать доступ к системной области, так что совместимость тоже пострадать не должна.
А если прибивать процесс сразу после попытки чтения памяти ядра, эксплойта уже не будет?
Будет. Там не обязательно спекулятивно исполнять после обращения к несуществующей памяти — это просто, чтобы пример упростить. Можно через предсказатель ветвлений сделать то же самое.
bool read_memory;
void* read_from;
if (read_memory) {
  tmp = *read_from;
  ...
}
Несколько раз читаем с простым, валидным, «нашим» адресом read_from, процессор запоминает и начинает исполнять ветку спекулятивно. Потом перекулючаем read_from на адрес ядра, а read_memory — на false. Ветка всё равно исполняется спекулятивно и данные адра спекулятивно же читаются — но «реального» исключения не происходит.

Кое-где используются эти прерывания для нормальной работы. Я такое видел в интерпретаторе Java.

Используются, но обращение идет не в область ядра.
Так и здесь обращение идёт не в адрес ядра! Посмотрите внимательно: то обращение, которое реально происходит и то, которое «ворует» данные — идут по разным адресам!
Приложение может и не пытаться читать из чужого процесса. Но могут так сложиться значения переменных, что если бы выполнилась альтернативная ветка условия (которая по логике программы не должна выполниться), то приложение обратилось бы к чужому адресу в памяти. И что, из-за этого приложение «прибивать»?
Кто-нидь может составить минимальный список литературы, чтобы понять что тут понаписано в комментах? а то чувствую «чайник закипает» ))
Долго искал, куда потеряли томик 2A. Оказывается, он стоит вертикально перед монитором.
А ARM Manula в таком виде? Книга на 6354 страницы… я хочу это видеть!
Видимо с чтением первого уже за вечер справился.
Можно начать с «архитектуры компьютера» Танненбаума. Про предсказание переходов хорошо рассказано даже в старом учебнике «Организация ЭВМ и систем» Цилькера.
Ага вот тут books кое-что нарыл
Если любой ассемблер изучить — то вцелом можно разобраться со всем остальным достаточно быстро.
Не, не, не
не с моей дислексией
поищу курс для полных идиотов на ютюб
спасибо
;o)
Добавлю немного от себя, точнее немного текста из книги «Оптимизация приложений на платформе .NET».

1) Микросхемы памяти типа DDR3 SDRAM дают задержки доступа к памяти порядка 15 наносекунд.
2) Процессоры за это время могут выполнить — десятки (а иногда сотни) инструкций. Явление простоя в ожидании доступа к памяти известно под названием удар о стену памяти.
3) Чтобы увеличить расстояние между приложением и этой «стеной», современные процессоры снабжаются несколькими уровнями внутренней кэш-памяти. L1 ~5 тактов CPU, L2 ~10 тактов, L3 ~40 тактов.
4) Когда процессор обращается к главной памяти (RAM), он читает из нее не один байт или слово, а строку кэша (cache line), размер которой в современных системах составляет 32 или 64 байта. Обращение к любому слову в той же строке кэша уже не будет вызывать промах кэша, пока эта строка не будет вытеснена из кэша.

Например, при суммировании элементов Int32 расположенных в массиве:
При обращении к элементу массива, вначале происходит промах кэша и загрузка строки кэша, содержащей 16 последовательно расположенных целых чисел (строка кэша = 64 байта = 16 целых чисел). Так как доступ к элементам массива выполняется последовательно, следующие 15 чисел оказываются в кэше и при обращении к ним не возникает промаха кэша. Это почти идеальный сценарий с соотношением промахов кэша 1:16.
По последнему примеру: процессор умеет определять, что обращения к памяти идут с постоянным шагом, и инициирует загрузку следующей строки кэша ещё раньше, чем к ней будет первое обращение. В итоге промахов будет ещё меньше, чем 1:16
Так себе обнова. С одной стороны — полезное замечание (да, действительно, ипользование оффсета 4096 действительно очень похоже на эксплуатацию TLB), с другой — совершенно безграмотные пассажи типа «все патчи уязвимостей Spectre и Meltdown предложенные производителями ПО это и делают, перезагружая в обработчике исключения GP регистр CR3» (притом что перезагрузка CR3 в обработчике исключений поможет против Meltdown и Spectre примерно как кровопускание против туберкулёза… а было ведь время, когда так и лечили).
А почему нельзя при возникновении любого исключения сбрасывать кэш? Исключения при обращении к запрещённым областям памяти, как я понимаю, всё равно сперва обрабатываются ядром ОС, вот в этом обработчике кэш и почистить перед тем, как передать управление дальше по цепочке обработчиков…
Тогда разве только сбрасывать весь кеш при откате результат спекулятивного исполнения о_О
Можно, но потеря производительности будет не 30%, а в разы. Типа возврат обратно к старому доброму 486 или в крайнем случае — старому Atom (до 2013г).
Опечатка к тексте во 2-ом упоминании Meltdown буква «t» пропущена
По-моему данная информация еще одна причина избавляться от использования языков, имеющих прямой доступ к памяти. На всяких там CLR или JVM проблем не будет
Откуда такая уверенность? В оригинальной работе про Spectre как раз один из примеров — эксплуатация уязвимости из JavaScript.
Для javascript проблема решается с помощью отключения SharedArrayBuffer, то есть с процессорами это мало связано, просто нефиг давать прямой доступ к памяти для стороннего кода. В JVM например по стандарту языка, любая выделенная память заполняется нулями. Особо не разгуляешься.
Для javascript проблема решается с помощью отключения SharedArrayBuffer
Неизвестно, сколько сайтов сломается.
В JVM например по стандарту языка, любая выделенная память заполняется нулями
Это защищает от чтения данных, оставшихся от предыдущего владельца. Для Spectre/Meltdown какая разница?
Я пример с нулями привел, как пример того, что при использовании java появление содержимого памяти другого процесса исключено. Помимо того, что исключен прямой доступ к памяти.
«Появление содержимого памяти другого процесса» в памяти вашего процесса исключено средствами ОС не зависимо от языка программирования (если мы работаем в user mode). Java дополнительно защищает только от случайного «воскрешения» данных своего же процесса.
Спасибо за статью! Очень понятно и лаконично!
Статья интересная и достаточно доходчивая — при чём краткая. Автор — молодец!

Но хотелось бы немного разбавить густые краски и снизить желание бегать за патчами и новыми процессорами :)

1. Уязвимость Meltdown (которая на Intel) позволяет локальному процессу с привилегиями пользователя прочитать (не записать, не изменить) память любого другого процесса, в том числе и память ядра.
2. Уязвимость Spectre (которая у всех процессоров) позволяет локальному процессу с привилегиями пользователя прочитать (не записать, не изменить) память любого другого процесса, но не память ядра.

Ключевое слово: «локальный процесс». Это означает, что основной риск существует для систем:

— которые запускают недоверенный код (то есть в первую очередь — систем с интернет-браузерами);
— которые запускают недоверенные процессы в песочнице, облаке и т.д.

По сути, если кто-то всю жизнь работал под админом и не использовал ограниченные учётки (думаю, это где-то 80% пользователей Windows точно) — для него всё осталось как и прежде, так что и беспокоиться не о чём.

А для остальных как обычно — не лазьте, где попало, не запускайте, что попало, поменьше скриптов, Java и т.д.

Показателем «критичности» является хотя бы то, что Mozilla быстренько выпустила хотфикс 57.0.4 с довольно невнятным описанием, но даже не почесалась для ESR.

В основном как всегда важна раздутость в СМИ, что как бы намекает на маркетинговую подоплёку — как же, новые процессоры надо как-то продавать, а на падение производительности надо как-то списывать :) Хотя имхо производительность современных устройств чуть ли не в последнюю очередь зависит от процессора.
Так, насколько я понял, мы не читаем напрямую адрес в запрещённой памяти — мы лишь смотрим какой из элементов массива попал в кэш и на основе этой информации делаем вывод о содержимом кэша.

В этой связи возникает несколько вопросов:
1. Верно ли, что для того чтобы вытащить что-либо осмысленное из памяти, нужно чтобы и атакуемая программа и атакующая достаточно времени должны выполняться параллельно, при этом атакующая на протяжении всего этого времени должна выполнять один и тот же, достаточно однообразный (и бессмысленный с точки зрения стороннего наблюдателя) паттерн? Если это верно, разве это не тривиальная задача для антивируса, если, конечно речь не идёт о «краже» одного байта?

2. Для чтения таким вот образом одного «чужого» байта, условно, нужно выполнить 256 своих? Разве это не чудовщиный пинок быстродействию при попытке реализации атаки и, в свою очередь, не очередной способ засечь атаку?

3. А что если реализовать «фрагментацию» кэша? Т.е. хранить в нём данные не линейно, а с разбивкой по адресам, например. Типа:

Элементы 0-125 хранятся по адресу 0x000AAD
Элементы 126-255 хранятся по адресу 0x000DDD

Разве что придётся какую-то таблицу с маппингом ещё хранить, но ведь это всё равно будет быстрее чем сбрасывать весь кэш, нет?

Скорее всего я не очень хорошо понимаю как всё внутри устроено, но мне почему-то кажется, что вытаскивая из кэша инфу по одному байту (учитывая что в этот кэш всю дорогу гадит каждая работающая на компьютере программа), ничего особо путного оттуда без плясок с бубном не извлечь. Атака выглядит слабореализуемой.
1. Нет, в Meltdown идёт чтение страниц памяти ядра, атакуемой программы вообще нету.
2. Да, но это можно оптимизировать. Засечь можно разве что слишком большое количество исключений, но это не является гарантией того, что программа пытается читать память ядра.
3. Совершенно непонятно, что вы имели в виду. Почитайте, как устроен ассоциативный кэш, например вот тут: habrahabr.ru/post/93263

В оригинальной статье авторы пишут, что смогли читать память ядра со скоростью около 500КБ/с, что, конечно, не быстро, но уж точно доказывает, что атака работает.
1. Эм… В таком случае, что даёт одно лишь чтение именно ядра?

3. Почитаю, спасибо.

Читать память ядра со скоростью 500 кб/с… Ну… Пока всё ещё это выглядит примерно так же как считывание информации с защищённого компьютера через вентилятор.
В памяти ядра есть вся замапленная физическая память, и все нужные структуры, чтобы прочитать в итоге память любого процесса.
А можно вкратце на простейшем примере — как?
М… Я может чего-то не понимаю, но это до сих пор всё ещё больше похоже на вытягивание рандомного мусора, чем на нормальную атаку.
Но у процесса есть свои менеджеры памяти
Что делать с этим массивом данных с кучей рандомных байт?
Что делать с этим массивом данных с кучей рандомных байт?
Если бы там были «рандомные данные», то процесс ничего путного сделать бы не смог. А так — там определённые структуры, списки… фактически всё, что может и «умеет» программа — там сидит.

Во времена DOS'а статьи, описывающие как «вытащить» из ядра DOS и разных программ полезные данные — были весьма популярны… и для нахождения полезной информации вовсе не нужно было сканировать всю память.

Потом, с переходом в защищённый режим, статьи писаться перестали (смысл какой, если защита вас всё равно в адресное пространство «чужого» процесса не пустит?). Сейчас, видимо, будет ренесса́нс…
Ок. сдаюсь — я старпер и к сожалению застал времена DOS+asm-x86
но вы точно не теоретик?
При чём тут «теоретик или практик», если кроме пресловутого RALF'а выходили целые книги описывающие внутренние структуры DOS'а? И туда народ «по привычке» активно лазал (на более ранних системах типа C64 описание чего где лежит было прямо в документации).
Ну, человек же не про структуры данных ядра сказал — про это и так можно почитать где угодно, начиная от Windows Internals, Undocumented 2000 (не помню точно название), заканчивая NT DDK и утекшими исходниками NT4 и 2000.
В своё время не только структуру данных ядра, но и самые распространённые TSR'ы описаны были. RBIL описывает далеко не только ядро DOS.
Всё не настолько плохо как можно подумать. У процесса, после передачи управления от планировщика есть довольно много времени чтобы реализовать атаку на определённую порцию данных и быть достаточно уверенным что нас не прервёт какой-либо процесс.
Главное это возможность атаки, даже если мы сможем за один квант процессорного времени выудить 1 байт нужной нам информации — это уже проблема для уязвимой системы. По 1 байту можно выудить все необходимые чувствительные данные. Причем нагрузки это особой не создаёт.
Данные же не статичны в большинстве случаев. Чаще всего они изменяются, а если по минуте тянуть каждый кусок, в нём никакого смысла не будет.
А если при возврате из ring0 в ring3 отключать отображение не всех ядерных адресов, а только страниц, содержащих «стартовые» данные, вроде системных параметров и заголовков списков, то нахождение осмысленной информации при атаках замедлится на порядки, а общая производительность приложений почти не пострадает.
Там просто перезагружается системный регистр CR3, что приводит к сбросу кэша TLB. Очищать по странице смысла нет, вирус будет искать очищеный кусок.
Что значит «искать очищенный кусок»?
Думаешь, в ядре там постоянно все данные с места на место переезжают и вообще всё кипит? Там много статики… и самые важные вещи довольно статичны.
UFO just landed and posted this here
Нет Win64, не могу попробовать…
Собрал, запустил. Результат, как на скриншоте — ни одного совпадения. Мало того, на чтение каждого байта на 6820HK уходит около 15 с, и загрузка процессора держится на уровне 12-14%, что сразу же заметно по динамике системы.
Если верить проге quess = real совпало 2 раза на ~100 (с патчем 0 на ~100). Случайность?!
Intel-овский детектор для 1-2-го семейства процессоров Intel® Core™ не работает (без драйвера Intel ME, а драйвер не устанавливается даже с заплаткой KB2685811). От 3-го семейства проблем не наблюдал.
CPU читает из RAM не побайтно, а кэш-линиями. Допустим CPU прочитал кэш-линию. Во время цикла
for (i = 0; i < 256; i++) {
if (is_in_cache(userspace_array[i*4096])) {
    // Got it! *kernel_space_ptr == i
}
}

мы определяем индекс массива, который читается быстро, но в то же время рядом с ним находятся еще несколько байтов из этой же кэш-линии и которые тоже будут быстро читаться.
Вопрос: как нам понять какой именно из этих индексов — нужное нам значение из закрытой области RAM?
обратите внимание, что i умножается на 4096, как раз для того, чтобы исключить этот эффект, и ещё несколько.
Да, все верно. Не уловил сразу.
Когда мы делаем
char not_used = userspace_array[tmp * 4096];
, то CPU читает кэш-линию (размером 64 байта) из RAM. После чего, при обращении к любому из этих байтов, время чтения будет минимально, т.к. они уже в кэше, что, собственно, и вызвало мой вопрос )) Но на самом деле нас интересуют только те индексы, которые кратны 4096.
Мы можем не дампить всю память. Достаточно прочесть таблицу процессов — и, вуаля, прочитав несколько килобайт у нас уже есть достаточно ценная для злоумышленника информация.

Далее, зная адрес каждого процесса, зловред попросту читает «уязвимые к чтению памяти» процессы. Т.е. практически любой софт (кроме супер-крипто-параноидальных) уязвим. KeePass вряд ли будет хранить пароли в чистом виде — они будут зашифрованы мастер-паролем. Но (самый примитивный вариант) прочесть содержимое текстового файла с паролями, открытого в блокноте — как раз плюнуть. Прочесть приватные ключи сертификатов — да легко. Прочесть вашу переписку в Скайпе — запросто. И.т.д.
KeePass вряд ли будет хранить пароли в чистом виде — они будут зашифрованы мастер-паролем.
Однако при этом или мастер-пароль или какий-нибудь хеш от него будут лежать в памяти рядышком. Иначе бы двойной щелчок мышью по имени сайта не мог бы скопировать в буфер клавиатуры пароль без повторного запроса мастер-пароля.

Вот если сессия закрыта (автоматом или по таймауту) — тогда уже всё. Но кто её закрывает…
Если бы заранее знать, что память процесса ненадёжна, то вот простой способ защититься: при запросе пароля он загружается с диска (с флагом «не кешировать»), расшифровывается, используется и удаляется из памяти. Эксплойт не имеет доступа к HDD.

Просмотр исходников KeePass2 показывает, что буферы хранятся в памяти, зашифрованные ф-цией ProtectedMemory.Protect из System.Security.dll со значением scope = SameProcess.

А вот где .NET держит ключи, отдельный вопрос. Не исключено, что они как-то дополнительно защищены.
UFO just landed and posted this here
Это к тому, как построить менеджер паролей, защищённый конкретно от Meltdown

Не все. Скажем страничка в браузере пока не имеет доступа. А это, наиболее уязвимый вариант, ввиду простоты запуска произвольного (в рамках JS AST) кода.

Тем временем некто на SecurityLab, ссылаясь на данную статью, объясняет, почему на самом деле всё не совсем так.
Ответил чуть ниже.
Всегда знал главную проблему Хабро-подобных ресурсов — желание развиваться (сосредоточенность на разумном деле) и желание трендеть о себе или почти о себе (смесь с ЧСВ) — вещи разных берегов, это очередное подтверждение: www.securitylab.ru/analytics/490642.php
Цитаты из оригинальной статьи:
Flush+Reload attacks work on
a single cache line granularity. These attacks exploit the
shared, inclusive last-level cache. An attacker frequently
flushes a targeted memory location using the clflush
instruction. By measuring the time it takes to reload the
data, the attacker determines whether data was loaded
into the cache by another process in the meantime.

As already discussed, we utilize cache attacks that allow
to build fast and low-noise covert channel using the
CPU’s cache. Thus, the transient instruction sequence
has to encode the secret into the microarchitectural cache
state, similarly to the toy example in Section 3.
We allocate a probe array in memory and ensure that
no part of this array is cached. To transmit the secret, the
transient instruction sequence contains an indirect memory
access to an address which is calculated based on the
secret (inaccessible) value. In line 5 of Listing 2 the secret
value from step 1 is multiplied by the page size, i.e.,
4 KB. The multiplication of the secret ensures that accesses
to the array have a large spatial distance to each
other. This prevents the hardware prefetcher from loading
adjacent memory locations into the cache as well.
Here, we read a single byte at once, hence our probe array
is 256×4096 bytes, assuming 4 KB pages.

Цитата из FLUSH+RELOAD: a High Resolution, Low Noise,
L3 Cache Side-Channel Attack
:
We observe that the clflush instruction evicts the
memory line from all the cache levels, including from
the shared Last-Level-Cache (LLC). Based on this observation
we design the FLUSH+RELOAD attack—an extension
of the Gullasch et al. attack. Unlike the original
attack, FLUSH+RELOAD is a cross-core attack, allowing
the spy and the victim to execute in parallel on different
execution cores. FLUSH+RELOAD further extends
the Gullasch et al. attack by adapting it to a virtualised
environment, allowing cross-VM attacks.
Two properties of the FLUSH+RELOAD attack make
it more powerful, and hence more dangerous, than prior
micro-architectural side-channel attacks. The first is that
the attack identifies access to specific memory lines,
whereas most prior attacks identify access to larger
classes of locations, such as specific cache sets. Consequently,
FLUSH+RELOAD has a high fidelity, does not
suffer from false positives and does not require additional
processing for detecting access. While the Gullasch et al.
attack also identifies access to specific memory lines, the
attack frequently interrupts the victim process and as a
result also suffers from false positives.
The second advantage of the FLUSH+RELOAD attack
is that it focuses on the LLC, which is the cache level
furthest from the processors cores (i.e., L2 in processors
with two cache levels and L3 in processors with
three). The LLC is shared by multiple cores on the
same processor die. While some prior attacks do use the
LLC [47, 60], all of these attacks have a very low resolution
and cannot, therefore, attain the fine granularity
required, for example, for cryptanalysis.
Судя по тону статьи на securitylab, её автор — тот ещё ЧСВ-шник )))
Ну там ещё и адекватные люди в комментариях отметились. И обнаружили как пассаж «For instance, taking care that the address translation for the probe array is cached in the TLB increases the attack performance on some systems» в оригинальной статье (что говорит нам о том, что реальные security-researfcher'ы из Google и других мест знают о TLB уж никак не меньше автора статьи… да и собственно на схеме CPU из этой статьи DTLB явно нарисован, его не «суперзнаток» из securitylab пририсовал), а также — проверка с другими константами. С 512 — работает надёжно, с 64 — тоже, но только если отключить prefetch в BIOS'е. А вот 32 — уже не работает. Стоит ли напоминать, что размер страницы — 4096 байт, а размер строки в кеше — 64 байта?

То есть наш д’Артанья́н не только не сделал открытия — он ещё и гидко обделался со своими «откровениями».
retry и jz retry нужны из-за того, что обращение к началу массива даёт слишком много шума и, таким образом, извлечение нулевых байтов достаточно проблематично. Честно говоря, я не особо понял, зачем так делать — я бы просто к rax прибавил единичку сразу после чтения, да и всё.

Судя по всему в некоторых случаях "подсмотренный" байт может занулиться до выполния shl.
jz retry нужен для того чтобы ингорировать неудачные для нас исходы race condition

Так были ответы на вопросы:
какие минимальные условия для атаки через сеть?
какое время для получения полезной инфы с взламываемого компа?
и только ли крадется инфа или можно еще нанести какой-то вред?
Это локальная атака.
Скорость чтения памяти ядра в Meltdown, по заявлению авторов — порядка 500КБ/с.
Можно только читать.
Всё это есть в оригинальной статье :)
Не смиись, не смешно ))
А то я уж подумал, ща все JS-девелоперы меня хакнут ))
Есть где-нибудь рабочий пример для win-64?
Возможно, я что-то упускаю (никогда не занимался анализом/эксплуатацией уязвимостей), но обнаружить код, потенциально использующий Meltdown, ядру проще простого, поскольку этот код должен сгенерировать хотя бы один страничный отказ, не обрабатываемый полностью в ядре (то есть, не влекущий за собой подкачки страницы). Следовательно, исключение будет передаваться пользовательскому коду, процедура эта небыстрая, а сама ситуация не может быть использована в эффективном и быстродействующем коде.

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

Таким образом (если я правильно понимаю) возможности эксплуатации уязвимости можно свести к уровню, неприемлемому для подавляющего большинства целей. Ну а глобальное отключение отображения адресного пространства ядра останется для параноиков.
сбрасывать/перезаписывать линии кэша, имеющие отношение к запрошенному адресу
Который не имеет отношения к тому адресу, на который производится атака.
вставлять задержки
Которые никого не волнуют.
включать на какое-то время режимы искусственного торможения потенциально опасного процесса
Который, в этот момент, уже «всё сделал» и готовится умереть.

Проблема в том, что в описанной схеме два процесса работают в тандеме — и тот, который вы можете легко обнаружить, к моменту вызова исключения уже является просто «отработанным материалом», а тот, который собирает дамп памяти — обнаружить не так-то просто. Да, им нужно быть связанными неким буфером — но это может быть, например, кусок кода libc или что-нибудь подобное…
Который не имеет отношения к тому адресу, на который производится атака.

Что значит «не имеет»? Атака производится именно на тот адрес, по которому впоследствии генерируется исключение.

Которые никого не волнуют.

Ну давайте считать, насколько волнуют. Для определения значения байта, считанного с атакуемого адреса, нужно выполнить в среднем 128 чтений из собственной памяти. При условии отсутствия в соответствующих адресов в кэше, каждое из этих чтений потребует, в лучшем случае, порядка сотни тактов — всего около 13000. Полный цикл IPI (запрос-реакция) отрабатывается примерно за 1500-2000 тактов. Если по каждому исключению, вызванному попыткой доступа к адресному пространству ядра, притормаживать через IPI все остальные ядра — уже есть неплохой шанс сорвать бОльшую часть атак.

Который, в этот момент, уже «всё сделал» и готовится умереть.

Что именно «все»? :) Даже если он к этому моменту и успел прочитать несколько байтов ядерных данных — что он с ними будет делать? Чтобы получить осмысленную информацию, оттуда нужно вычитывать довольно много, причем делать это побыстрее, пока не перестроились списки и не поменялись указатели. Атака на отдельные переменные ядра практического смысла не имеет.

Да, им нужно быть связанными неким буфером — но это может быть, например, кусок кода libc или что-нибудь подобное…

Им в любом случае не обойтись без ядерных средств межпроцессного взаимодействия. Для ускорения анализа можно для каждого процесса держать список связанных с ним, а при появлении в системе нескольких связанных друг с другом пользовательских процессов к ним можно изначально проявить и более пристальное внимание.

Это все будет гораздо правильнее, чем тупо отключать адресное пространство ядра при каждом возврате в user mode (но для особо критичных случаев сгодится и это).
Что значит «не имеет»? Атака производится именно на тот адрес, по которому впоследствии генерируется исключение.
Сейчас — да. Но если вы видели как Spectre реализуется, то могли бы понять, что можно вообще без исключений обойтись. А уж сделать два обращения в память так, чтобы первое «било» в молоко — так и вообще не проблема.

Им в любом случае не обойтись без ядерных средств межпроцессного взаимодействия.
Нет. Им нужен кусок разделяемой памяти — и всё. Все обращения в память ядра можно сделать спекулятивными. Хоть это и несколько усложнит логику.
Статья прежде всего про Meltdown, и я говорю прежде всего о нем. О Spectre разговор отдельный.

Куда какое обращение «бьет», для Meltdown разницы нет — исключение возникает всегда.

Если у процессов есть разделяемая память, доступная для записи — это и означает наличие межпроцессного взаимодействия. У независимых процессов такой памяти никогда нет.
Статья прежде всего про Meltdown, и я говорю прежде всего о нем.
Так и я о нём же.

Куда какое обращение «бьет», для Meltdown разницы нет — исключение возникает всегда.
Исключение вызывается только для «реальных» обращений. Сделайте обращения к «ядрёной» памяти спекулятивными — всё. Скорость упадёт, но никаких исключений не будет.

Если у процессов есть разделяемая память, доступная для записи — это и означает наличие межпроцессного взаимодействия.
Зачем вам память, доступная для записи? Двухмегабайтный массив, через которые данные передаются Meltdown только читает.

У независимых процессов такой памяти никогда нет.
Зависит от операционки, но этого и не нужно. Почти все процессы разделяют, как минимум, libc. А можно и что-нибудь более редкое загрузить.

Ваш подход — это подход антивирусников: отловим существующий паттерн и подождём пока вырусы что-нибудь новенькое изобретут. Хорошо для job security, не очень хорошо для безопасности.
Так и я о нём же.

Вы не о Meltdown, а о чем-то типа Spectre.

Исключение вызывается только для «реальных» обращений. Сделайте обращения к «ядрёной» памяти спекулятивными — всё. Скорость упадёт, но никаких исключений не будет.

Что значит «сделайте спекулятивным»? «Чисто спекулятивное» обращение получается только в атаках типа Spectre. В Meltdown результат обращения используется для индексации, поэтому процессор не имеет оснований считать это обращение исполняемым спекулятивно и не проверять прав доступа.

Если знаете способ избежать исключения — опишите.

Зачем вам память, доступная для записи?

Как иначе предлагаете делать синхронизацию между процессами? Общие примитивы синхронизации опять-таки видны ядру.

Почти все процессы разделяют, как минимум, libc

И что это может им дать в плане синхронизации при атаке?

Ваш подход — это подход антивирусников

Это подход прагматика — минимизировать следует лишь наиболее значимые риски. Если вероятность успешной атаки через спекулятивное выполнение упадет ниже вероятности успешной атаки через другие уязвимости — значит, нет практического смысла снижать ее дальше. А для особо критичных систем, перфекционистов и параноиков всегда останутся радикальные способы, сильно снижающие производительность.
IntelR 64 and IA-32 Architectures
Software Developer's Manual
Volume 3A: System Programming Guide, Part 1
Раздел 11.3 (Таблица 11-2).
Strong Uncacheable (UC) — System memory locations are not cached. All reads and writes appear on the system bus and are executed in program order without reordering. No speculative memory accesses, pagetable walks, or prefetches of speculated branch targets are made.

Храните деньги приватные данные в сберегательной кассе в некэшируемых страницах, и спите спокойно.
Для тех, для кого не важна производительность есть Pentium MMX…
UFO just landed and posted this here
Самое смешное, что тормоза менеджера паролей (если, конечно, он написан не очень криворукими людьми) Вы вряд ли сумеете ощутить, даже если выполнение замедлится в десятки раз. Большинство плееров тоже не пострадает, ибо они работают с большими (сотни миллисекунд) буферами, обработка которых выполняется преимущественно в режиме ядра. А вот всякие low-latency приложения, использующие буферы на единицы миллисекунд, да в каких-нибудь десятках с их неимоверными накладными расходами, уже могут начать потрескивать.
Обратите внимание, что в кэш подтягивается строка из userspace_array. А будут ли те данные, которые располагаются по адресу из kernel_space_ptr, в кэше или нет — без разницы.
Рассмотрим ещё раз код атаки:
; rcx = kernel address
; rbx = probe array
retry:
mov al, byte [rcx]
shl rax, 0xc
jz retry
mov rbx, qword [rbx + rax]

Если память по адресу [rcx] кэширована, с высокой вероятностью спекулятивно будут выполнены строки до
mov rbx, qword [rbx + rax]
включительно.
Если память по адресу [rcx] некэширована, произойдёт спекулятивная подкачка кэша, вероятность выполнения указанной выше строки уменьшится.
Но если память по адресу [rcx] некэшируемая, спекулятивное выполнение прервётся на строке
mov al, byte [rcx].

Перечитал ещё раз, согласен — выглядит как решение. Интересно, можно ли использовать для защиты от spectre.
Менять MTRR? Интересная мысль! Надо тогда вынести ISR из ядра.
Не, не годится — это же надо выключать ядро во время выполнения, а оно не стоит того.
Не надо трогать MTRR. Достаточно отключить кэширование чтения из памяти конкретной страницы — выбрать для страницы (PTE) PCD =1, или детально разобраться в механизме PAGE ATTRIBUTE TABLE (PAT), (11.5.2 Precedence of Cache Controls).
The PAT allows any memory type to be specified in the page tables, and therefore it is possible to have a single physical page mapped to two or more different linear addresses, each with different memory types.
Ага, сделать какой нибудь специальный флажок на mmap, который будет выделенные страницы помечать таким образом — и в userspace приложениях можно будет выделять «защищённую» память для критически важны данных.
В ядре linux есть специальная функция, которая правильно размечает страницы: elixir.free-electrons.com/linux/v4.6/source/arch/x86/mm/pageattr.c#L1517
Кстати, начинают появляться первые утилиты на проверку наличия уязвимости: www.ashampoo.com/en/usd/pla/1304/security-software/spectre-meltdown-cpu-checker

Не от самых маститых разработчиков, и не самые стабильные в работе — но всё же.
Более того: к работе тоже много вопросов :)
О да: утилита знатная: похоже стучит на 184.25.216.99 или 191.234.42.116 и проверяет тип процессора на уязвимость :)
Без выхода в интернет не работает.
Интересно, а для эксплуатирования Meltdown на старых INTEL CPU программный код будет отличаться? Т.е. надо ли для каждого отдельного проца писать свой код или он будет одинаковый для всех INTEL CPU?
Теоретически должно работать прямо так, если объёма кэша хватит. Ну и «старые» — это хотя бы core 2.
Ну и «старые» — это хотя бы core 2.
Если бы.

Просто чем дальше в прошлое тем меньше спекуляций делает процессор и тем сложнее их эксплаутировать. Начиная с Core 2 просто эксплоит пишется легко, но теоретически — должно быть возможно поиметь проблемы уже на Pentium Pro.

Articles