Новогодние подарки, часть первая: Meltdown
Да, я знаю, что это уже третий материал на GT/HH по данной проблеме.
Однако, к сожалению, до сих пор я не встречал хорошего русскоязычного материала — да в общем и с англоязычными, чего уж тут греха таить, та же проблема, там тоже многих журналистов изнасиловали учёные — в котором внятно раскладывалось бы по полочкам, что именно произошло 3 января 2018 года, и как мы будем с этим жить дальше.
Попробую восполнить пробел, при этом и не слишком влезая в глубины работы процессоров (ассемблера не будет, тонких подробностей постараюсь избегать там, где они не нужны для понимания), и описывая проблему максимально полно.
Тезисно: в прошлом году нашли, а в этом опубликовали информацию о самой серьёзной ошибке в процессорах за все десятилетия их существования. В той или иной степени ей подвержены все процессоры, используемые в настоящее время в настольных компьютерах, серверах, планшетах, смартфонах, автомобилях, самолётах, поездах, почте, телефоне и телеграфе. То есть — вообще все процессоры, кроме микроконтроллеров.
К счастью, подвержены они ей в разной степени. К несчастью, самый серьёзный удар пришёлся на самые распространённые процессоры — Intel, причём затронул он абсолютно все выпускающиеся и практически все эксплуатируемые (единственным исключением являются старые Atom, выпущенные до 2013 года) процессоры этой компании.
До сих пор вся система обеспечения информационной безопасности на локальном компьютере в основе своей полагалась на одну предпосылку: центральный процессор способен гарантированно обеспечить полную изоляцию выполняющихся на нём программ друг от друга, поэтому при условии отсутствия ошибок в ПО различные процессы не имеют доступа к данным друга друга, если такой доступ не был предоставлен в явном виде.
Уберите эту предпосылку — и всё остальное рушится, как карточный домик.
Конкретно в архитектуре x86 защищённый режим был представлен в процессорах 80286 и доведён до ума в 80386. При работе в нём приложения вместо прямого доступа к физической памяти получают виртуальные адресные пространства, отображаемые на физическую память так, что пространства разных приложений не пересекаются. Контроль за адресными пространствами реализован аппаратно — блоком MMU, Memory Management Unit — поэтому, если при запуске нового процесса ОС корректно выделила ему память, попытка вылезти за пределы этой памяти будет немедленно пресечена процессором. Кроме того, участки памяти могут иметь разные уровни доступа, контроль за которыми также осуществляет MMU — в результате пользовательское приложение не сможет получить доступ к памяти, занимаемой ядром системы или драйверами, даже если соответствующие адреса формально ему доступны.
Всё было хорошо, пока не выяснилось, что сам процессор в ходе отработки своих внутренних алгоритмов может целенаправленно игнорировать MMU, а результаты работы этих алгоритмов могут быть косвенными путями получены программно.
Все современные процессоры — если мы говорим конкретно про Intel x86, то это модели начиная с Atom 2013-го года, Pentium Pro и Pentium II — имеют функции спекулятивного выполнения инструкций и предсказания ветвлений, а также кэши инструкций и данных.
Эти алгоритмы необходимы для обеспечения высокой производительности всей системы при условии, что все имеющиеся периферийные устройства, включая оперативную память, значительно медленнее центрального процессора. В случае, когда очередная выполняемая инструкция приводит к необходимости ожидания поступления данных снаружи, в течение периода ожидания процессор вместо простаивания начинает выполнять следующий за данной инструкцией код, исходя из ожиданий о том, какие именно данные с наибольшей вероятностью поступят.
Например: if (x < array1_size)
{
y = array1[x]
}
При последовательном выполнении этого кода процессор сначала проверяет, что переменная x находится в пределах массива array1, а потом выполняет вторую строку. При спекулятивном выполнении после какого-то количества if’ов, в которых действительно x < array1_size, процессор начинает считать, что данное условие с высокой вероятностью выполняется. поэтому начинает выполнение второй строки, не дожидаясь вычисления условия.
Что случается, если результат вычисления условия вдруг оказывается негативным? Процессор просто отбрасывает все предварительно вычисленные результаты и проводит вычисления заново, на этот раз уже последовательно. Одновременно модуль предсказания ветвлений снижает оценку вероятности того, что данное условие будет выполняться.
Что случается, если переменная x оказывается расположенной не просто за пределами конкретного массива, а вообще за пределами доступной данному процессу памяти? Процессор всё равно выполняет вторую строку кода. Дело в том, что на работу модуля MMU, в задачу которого входит определение, разрешено ли вообще данному процессу что-то читать по адресу x, также нужно время, поэтому механизм спекулятивного выполнения относится к MMU ровно тем же образом, как и к внешним шинам — он начинает выполнение до того, как из MMU поступит ответ о корректности этого кода.
Если MMU сообщает, что код некорректен, в эту память нельзя — процессор просто сбрасывает всё насчитанное.
Длина «забегания вперёд» у современных процессоров легко может составлять десятки инструкций.
На этой стадии всё пока ещё хорошо. Программа не знает о том, что происходит внутри процессора, если спекулятивное выполнение было самим процессором признано неудачным — его результаты ни одним из формальных путей в приложение не попадают (они не сохраняются ни в ОЗУ, ни в регистрах процессора). Всё, что можно зафиксировать снаружи — небольшую просадку темпа выполнения инструкций в момент, когда процессор понял, что кусок кода надо выполнить заново, и сбросил уже посчитанный результат и загруженные на конвейер команды (кто помнит дискуссии вокруг производительности Pentium 4, земля ему пухом, — там как раз был очень длинный конвейер, сброс которого обходился, соответственно, довольно дорого).
Но у процессора есть ещё один блок, и этот блок работает полностью самостоятельно — это кэш. В кэш ложатся все данные, прошедшие через процессор. Кэш, повторюсь, совершенно автономен — он не знает и не хочет знать, почему и как эти данные в него свалились, были ли они загружены в результате спекулятивного или последовательного выполнения, было ли спекулятивное выполнение признано корректным или нет. Данные пришли — данные легли в кэш.
То есть, если в рамках спекулятивного выполнения команд случится следующая последовательность:
- Прочитано значение по адресу в памяти N
- Получен ответ от MMU о невалидности данного адреса в контексте данного процесса
- Результаты чтения адреса сброшены из регистров процессора
То в кэше спокойно останется лежать значение, хранящее по адресу N.
Но и этого ещё мало, так как кэш — это внутреняя сущность процессора, ПО не может напрямую его читать. Однако, если ПО снова обратится по адресу N, то по скорости ответа процессора оно сможет определить, хранится ли соответствующее значение в кэше (быстрый ответ) или нет (медленный ответ).
И вот тут мы подбираемся к интересной части: ПО всё ещё не может напрямую прочитать некий адрес N, находящийся за пределами его доступа, но уже может определить, читался ли этот адрес кем-либо ранее.
Но ведь наше ПО по-прежнему не может в открытую читать адрес N, не так ли? Так. Но тут всё уже совсем просто: у современных процессоров есть процедуры косвенной адресации, указывающие, что процессор должен прочитать значение X, лежащее по адресу Y, а потом — значение Z, лежащее по только что прочитанному X. Ну да, работа с указателями.
Представьте, что у нас есть доступная приложению область памяти, поделённая на два куска с разными приоритетами — у одного приоритет собственно приложения, у другого приоритет ядра. Так делают до позавчерашнего дня делали по умолчанию, чтобы приложению не приходилось далеко ходить за системными вызовами, уходящими в ядро — дёрнуть что-то из того же куска виртуальной памяти намного быстрее, чем ходить каждый раз в другой кусок, а приоритеты решают проблему запрета приложению на прямой доступ к этому куску.
И, допустим, приложению в этом куске доступны адреса 0…9999, а ядру — 10000…20000, цифры тут неважны. И мы делаем конструкцию, расположенную в коде так, что процессор заведомо выполнит её в спекулятивном режиме (например, на тысячном повторении одного и того же цикла, 999 предыдущих повторений которого выполнялись корректно), и которая будет представлять собой косвенную адресацию по значению, лежащему по адресу 15000.
- Процессор в спекулятивном режиме читает значение по адресу 15000. Пусть там будет лежать, например, 98.
- Процессор читает значение, лежащее по адресу 98.
- От MMU приходит ответ о невалидности адреса 15000.
- Процессор сбрасывает конвейер и вместо значения, лежащего по адресу 98, выдаёт нам ошибку.
- Наше приложение начинает читать адреса от 0 и выше в собственном адресном пространстве (имеет полное право), замеряя время, требующееся на чтение каждого адреса, и читая их не по порядку, чтобы не натренировать тот же спекулятивный доступ
- На адресе 98 время доступа вдруг оказывается в несколько раз ниже, чем на других адресах
Таким образом мы понимаем, что кто-то уже недавно читал что-то по этому адресу, в результате чего он попал в кэш. Кто бы это мог быть? Ах, да, это наш дорогой процессор. По адресу 15000, соответственно, лежит значение 98.
Таким образом мы можем прочитать всю память ядра системы, на которую, в свою очередь, в современных ОС отображается вообще вся физическая память компьютера.
Это называется Meltdown (CVE-2017–5754), и эта уязвимость полностью перечёркивает все имеющиеся в процессоре защитные механизмы.
Кто подвержен Meltdown?
Как минимум все процессоры Intel линейки Core, все процессоры Intel линейки Xeon, все процессоры Intel линеек Celeron и Pentium на ядрах семейства Core, а также процессоры на ядрах ARM Cortex-A15, Cortex-A57, Cortex-A72 и Cortex-A75.
Кто не подвержен Meltdown?
Здесь чуть вопрос сложнее, поэтому сначала переформулируем его так: на данный момент не удалось — и, возможно, не удастся никогда — показать уязвимость Meltdown на процессорах AMD и ARM Cortex, отличных от перечисленных выше.
В оригинальной работе, публично открывающей подробности Meltdown, указывается, что на этих процессорах также были замечены изменения состояния кэша в результате спекулятивного выполнения инструкций, однако полезные данные получить не удалось.
Возможно, логика работы данных процессоров такова, что практическая реализация уязвимости на них невозможна вообще — например, MMU успевает прервать спекулятивное выполнение до того, как процессор прочитает вторую ячейку памяти; в этом случае в кэше окажется значение первой ячейки (лежащей в недоступной нам области), что не позволит эксплуатировать уязвимость.
Про кого мы не знаем, подвержен ли он Meltdown?
Есть масса процессоров, которые ещё толком никто не проверял — как минимум потому, что «оглушающий успех» Intel затмил всех остальных. С хорошей вероятностью не подвержен MIPS. Про более маргинальные архитектуры сказать ничего нельзя, и, вероятно, до них ещё долго никто не доберётся.
Если вас интересуют отечественные процессоры, то у Эльбруса своя архитектура, Байкал-Т1 построен на ядре MIPS P5600, Элвис — тоже на MIPS. С большой вероятностью они не подвержены Meltdown, хотя пока что в открытом виде информации об этом нет. Байкал-М построен на ядре Cortex-A57 и, соответственно, подвержен Meltdown.
Можно ли защититься от Meltdown программно?
Да. Meltdown эксплуатирует игнорирование процессором уровней доступа к памяти, но не позволяет читать память, находящуюся за пределами выделенного приложению виртуального блока.
Для всех распространённых ОС уже вышли или готовятся выйти патчи, переносящие область памяти ядра в другую облась, т.е. обеспечивающие защиту не только выставлением привилегий, но и контролем доступа по адресам — второе Meltdown обойти не может.
Проблема в том, что при такой архитектуре системные вызовы (syscalls) становятся крайне накладными — они замедляются в несколько раз. Соответственно, реальные приложения также теряют в производительности, в зависимости от доли системных вызовов в конкретном приложении падение может составить от 1–2 до нескольких десятков процентов. Например, на PostgreSQL продемонстрировано падение более 20%, в то время как в игрушках разницы практически нет.
Меньше всего угроза для игр и иных пользовательских десктопных приложений, так как они мало используют системные вызовы — в абсолютном большинстве из них падение производительности не превысит 3%, что можно считать погрешностью измерений. Впрочем, всегда могут быть исключения в лице отдельных приложений.
По этой причине данное поведение в ОС, скорее всего, будут включать только на процессорах, про которые известно, что они подвержены уязвимости. Тут тоже могут быть нюансы: например, в свежих патчах для ядра Linux есть не чёрный список уязвимых, а белый список неуязвимых, и в него пока входят только AMD.
Когда не нужно защищаться от Meltdown?
Meltdown относится к сугубо локальным атакам и подразумевает выполнение кода на вашем компьютере. Если никакой посторонний код, включая высокоуровневые языки, проникнуть на конкретный компьютер не может, Meltdown не опасен.
При этом не надо забывать, что Javascript в браузере — это тоже локально выполняющийся код. Разработчики браузеров сейчас предпринимают усилия по снижению эффективности использования JS для подобных атак, например, сознательно загрубляют доступные таймеры так, чтобы JS-код не смог достоверно измерить время доступа к памяти, но эти меры лишь снизят эффективность атак, но не исключат их полностью.
Если у вас стоит домашний сервер, который доступен только по сети, а софт на него вы устанавливаете исключительно из репозитария вашего любимого дистрибутива, то опасность для него будет заключаться только в сочетании Meltdown с каким-либо классическим эксплоитом, позволяющим подсадить вам «жучка» удалённо. Для атакующего удобство Meltdown будет заключаться в том, что с ним «жучку» не обязательно иметь какие-либо конкретные привилегии на атакуемой системе, достаточно запуститься на ней любым образом. С другой стороны, Meltdown позволяет только читать память, но не изменять её или выполнять какой-либо код. С третьей стороны, так как до недавних пор память считалась сравнительно безопасным местом временного хранения данных, то все ваши пароли и сертификаты лежат в ней в открытом виде.
Поэтому лучше поставьте патч сразу, как только он будет доступен для вашей ОС.
Несколько печальна может оказаться ситуация со смартфонами на Android, половина производителей которых известно в какого цвета тапочках видела все эти обновления — но, с другой стороны, у ARM подвержены Meltdown только новые старшие ядра, в то время как большинство неподдерживаемого ширпотреба сделано на старых и/или упрощённых ядрах.
Покажите мне, как вы с этим рутовый доступ получите!
Самое бессмысленное требование в дискуссиях о Meltdown. Уязвимости процессора не требуют обращения к верхнеуровневым абстракциям операционной системы — они даже не обходят их, они их просто не замечают.
В этом и есть самая большая опасность эксплоита Meltdown — он никак не взаимодействует с операционной системой, а значит, не только практически не зависит от неё, но и не оставляет в ней никаких следов.
Смогут ли антивирусы определять эксплуатирующее Meltdown ПО?
В общем случае — нет. В частном — они смогут определять ПО, про которое иными путями стало известно об эксплуатации им уязвимости, но в самом коде ничего однозначно нелегитимного нет, так что попытка эвристического определения может привести к большому количеству ложноположительных срабатываний.
Придумало ли Meltdown ЦРУ или KGB?
Нет, им не надо. Если они очень захотят заложить в ваш процессор закладку — они вежливо попросят производителя добавить какой-нибудь специализированный модуль, по типу JTAG, который спокойно позволит кому положено читать что ему надо напрямую. Сложность современных ядер такова, что этот модуль никто вообще никогда не заметит, а если и заметит, то у него прошивка всё едино будет в зашифрованном виде «для обеспечения безопасности наших пользователей».
Сделала ли Intel это специально, чтобы мы купили новые процессоры?
Разумеется, нет. Во-первых, глупо предполагать, что Intel запланировала это за двадцать лет до реального использования и в надежде, что никто раньше не обнаружит случайно. Во-вторых, цикл проектирования и выпуска нового процессора — не меньше пары лет (что неплохо видно по выпуску железа на лицензируемых ядрах, будь то ARM или MIPS; в случае AMD или Intel мы не знаем, когда реально было закончено проектирование ядра, а видим только работу маркетингового отдела, по которой создаётся впечатление, что там всех дел на полгодика — тут, как говорится, «хорошо вам, товарищ политрук, рот закрыл — рабочее место убрано»). В-третьих, кстати, про ARM, на примере которого видно, что уязвимость может появиться в ядрах процессоров в любой момент, в том числе в новейших топовых моделях — говоря проще, до сих пор никто не задумывался о функционировании связки из MMU, кэша и предсказания ветвлений как о потенциально уязвимой области, а выбирал их алгоритмы, исходя из иных предпосылок. В-четвёртых, трудно представить удара по Intel больнее, чем быстрый выпуск линейки исправленных процессоров: влияние этого на продажи моделей, которыми уже завалены склады, представляете?
Так что готовьтесь к тому, что 9-е поколение Intel Core будет иметь ту же уязвимость в полном объёме и полном составе.
Хотя многие действительно рано или поздно задумаются о новом процессоре. Подозреваю, в AMD сейчас по этому поводу второй раз за неделю фейерверки и шампанское.
TL: DR
Полная жопа, самая эпическая дыра за последние двадцать лет, но заплатка на ОС проблему устраняет полностью, пусть и ценой некоторой потери производительности. Владельцы AMD могут пока не волноваться.
А что там со Spectre?
Сейчас напишу отдельным текстом, ибо тема Spectre ещё обширнее.