Привет! Без лишнего: в статье расскажу про атаки на кэш-память в процессорах семейства ARMv8. Подробно изучил их для совершенствования безопасности KasperskyOS: познакомлю с теорией и практикой, механизмами работы и способами митигации. Также кратко расскажу, как мы тестировали каждый способ атаки на KasperskyOS, какие из них оказались неприменимы, какие могут представлять угрозу и как микроядро с подобными угрозами справляется. Если интересно гранулярно погрузиться в типологию атак на кэш — добро пожаловать!
Первый раздел про кэш и его особенности — ключевой для понимания контекста. Однако если вы уже отлично в курсе, например, какое важное для атак свойство гарантируют и Non-aliasing VIPT, и PIPT, можно сразу переходить к разбору конкретных атак.
Про кэш-память
Когда скорость процессоров стала в сотни раз превосходить скорость основной памяти, появилась необходимость в промежуточном хранении данных. Кэш-память процессора берет данные на буферное хранение из RAM: кэш меньше, но быстрее RAM.
Кэш-память скрывает разницу в скорости между основной памятью и процесcором. Без нее скорость основной памяти ограничивала бы время работы процессора: прикладные программы не только перемалывают регистры, а часто обращаются к данным в памяти.
Производители процессоров часто разделяют кэш-память для данных и для кода. Основная идея такого разделения в повышении производительности: такой подход позволяет параллельно читать и данные, и код. Также разделение позволяет выбирать разные подходы для кэшей данных и кода, так как паттерны доступа к ним разные — к коду процессор обычно обращается почти линейно, т. е. инструкция за инструкцией, а к данным часто в хаотичном порядке. Кэш для данных обозначают как DL$, а кэш инструкций как IL$, где $ — номер уровня.

Среднее количество тактов может варьироваться в зависимости от железа, но относительная разница в скорости доступа — важный фактор.
Скорость самой кэш-памяти также неоднородна. Сейчас обычная конфигурация — от 2 до 3 уровней кэш-памяти. Каждый обозначается как L$N, где $N — номер уровня.

Чем быстрее кэш-память, тем меньше данных она может хранить. Деление на уровни позволяет хранить больший объем данных и при этом иметь доступ к ним быстрее, чем к основной памяти.
Часть уровней приватна для каждого отдельного ядра, часть может быть общей между всеми. Первый общий между всеми процессорами уровень кэш-памяти называется LLC, Last Level Cache.
Он всегда последний?
Last Level необязательно последний в иерархии уровней. Например, в процессорах семейства Intel есть второй уровень общего кэша — L4, служащий для общения с видеокартой. Но это выходит за рамки текущей статьи.
Эксклюзивность и инклюзивность
Каждый уровень кэш-памяти, кроме первого, может иметь различные свойства эксклюзивности по отношению ко всем уровням выше. Всего существует три класса:
Инклюзивный (inclusive): если кэш-линия находится в уровне кэша, она обязана находиться в инклюзивной кэш-памяти уровня ниже.
Если кэш-линия убирается из инклюзивной кэш-памяти уровня N, все копии этой кэш-линии убираются из кэш-памяти с уровнями < N.
Эксклюзивный (exclusive): если кэш-линия находится в эксклюзивном кэше уровня N, копии этой линии не попадают на уровни < N.
Неэксклюзивный и неинклюзивный (non-inclusive non-exclusive): любая вариация расположений кэш-линий возможна.
Свойство инклюзивности важно для атак на кэш с использованием инструкций сброса.
Кэширование данных
Кэш-память хранит данные не побайтно, а кэш-линиями, блоками одинакового размера, например по 64 байта. Довольно часто доступ к данным, которые находятся рядом в памяти, происходит последовательно, поэтому был выбран такой способ.
Кэш-линии группируются в наборы (на английском set): в каждом от 4 до 16 линий в зависимости от уровня и типа кэш-памяти. Такие кэши называются ассоциативными.

Объем кэша меньше, чем объем всей памяти, поэтому в один и тот же набор могут попадать разные адреса.
Можно ли здесь построить биективное отображение?
Для построения биективного отображения оба множества должны быть равномощны, что в данном случае не выполняется.
Для того чтобы отличать принадлежность кэш-линии к адресу, к каждой линии приписан tag.
Non-aliasing VIPT и PIPT
Современные кэши делятся на два типа.
Non-aliasing VIPT (Virtually Indexed Physically Tagged): номер набора вычисляется из виртуального адреса, а тег из физического.
Свойство non-aliasing гарантирует, что если разные виртуальные адреса, указывающие на один физический, попадают в разные наборы, то консистентность данных не будет нарушена.
Обычно это обеспечивает железо через snooping. Либо количество наборов уменьшается так, чтобы индекс всегда брался из одинаковой части виртуального и физического адреса.
PIPT (Physically Indexed Physically Tagged): номер набора и тег вычисляются из физического адреса. Все виртуальные адреса, указывающие на одинаковый физический, всегда оказываются в одном наборе.

Оба варианта гарантируют очень важное для атак свойство — когерентность данных для разных виртуальных адресов, указывающих на один физический.
Инструкции для работы с кэш-памятью
Некоторые архитектуры позволяют манипулировать кэш-памятью при помощи специальных инструкций. Например, cflush на x86 и семейство инструкций dc и ic на ARMv8.
Почему два семейства?
dc — для кэша данных
ic — для кэша инструкций
Операции над кэш-памятью можно разделить на две группы.
Инвалидация: кэш-линия, к которой принадлежит адрес, обозначается как пустая. Данные остаются в кэш-памяти, но когда процессор попытается получить к ним доступ, ему придется идти в основную память, чтобы перечитать новое значение.
Сброс: данные кэш-линии копируются на уровень ниже, то есть в следующий уровень кэша или даже в основную память.
Часто инструкции делают оба действия за один раз: данные уходят на уровень ниже, а кэш-линия обозначается как пустая. В большинстве ситуаций у разработчиков нет необходимости в инструкциях сброса кэша: они скорее приведут к потере производительности.
А вот драйверам устройств, которые работают с некогерентной DMA-памятью, они часто полезны. В их случае кэш сильно мешает общению устройства и прикладного кода: устройство работает напрямую с физической памятью, а прикладной код с ней же, но через посредника, кэш-память. Также использовать эти инструкции могут JIT-компиляторы.
Поэтому в таких случаях стоит быть аккуратными: не забывать инвалидировать или сбрасывать кэш, чтобы удостовериться, что обе стороны видят одинаковые данные.
Степень доступа к инструкциям сброса из пространства пользователя зависит от архитектуры. Например, на х86 cflush всегда доступна из пространства пользователя, и с этим ничего не сделать. На ARMv8 ядро может либо разрешить, либо запретить использование таких инструкций.
В KasperskyOS за это отвечает конфигурация ядра CONFIG_ARM64_ALLOW_USERSPACE_CACHE_MAINTENANCE. Когда эта опция включена, инструкции семейства dc и ic доступны из пространства пользователя. В противном случае ядро предоставляет вызов KnHalFlushCache: тот же результат, но уже в пространстве ядра. Однако вызов покрывается политиками безопасности, что не позволит вредоносным процессам использовать ее.
Это все нужные для понимания атак детали о кэше, переходим собственно к атакам.
Типы атак
Можно выделить два основных типа атак на кэш-память: атаки Rowhammer и атаки, проходящие по побочным каналам. Рассмотрим их подробнее.
Rowhammer
Тип атаки на оперативную память (DRAM), может привести к несанкционированному доступу к данным.
DRAM организована в виде рядов и столбцов, а Rowhammer использует повторяющиеся операции чтения или записи в один и тот же ряд памяти.
Создаваемое электромагнитное поле влияет на соседние ячейки памяти, изменяя их содержимое. Злоумышленники могут использовать эту уязвимость чтобы изменить данные или даже запустить вредоносные программы, это серьёзная угроза безопасности.
Так как между современным процессором и оперативной памятью существует промежуточный слой в виде кэша, запись напрямую в DRAM сильно затруднена. Однако если создать некэшируемую трансляцию или использовать инструкции сброса кэша, можно писать напрямую в основную память с достаточной скоростью и эксплуатировать уязвимость.
Цель подобной атаки — важные структуры данных ядра, которые могут дать атакующему некие привилегии. Например, получив доступ к таблице трансляций и перезаписав бит в PTE, который не позволяет читать страницы из пространства пользователя, атакующий получит доступ к данным ядра.
Углубление в single-edge и double-edge
Существует два типа атаки: single-edge и double-edge. Отличие в том, сколько соседних ячеек используется для изменения содержимого ячейки.
В случае single-edge используется одна ячейка и содержимое может поменяться в любой из соседних (на картинке A это ячейка, в которую производится запись, а оранжевые — это те, которые являются целью атаки)


Атаки по побочным каналам
Такие атаки используют особенности кэширования для организации непредусмотренного канала связи по времени. Поскольку доступ к данным, находящимся в кэше, быстрее, чем запрос к памяти (или кэшу более низкого уровня), измерение времени доступа позволяет понять, что к данным уже обращались.
Довольно распространенный тип атак. Процессоры значительной части современных устройств поддерживают кэш-память: персональные компьютеры, ноутбуки, смартфоны и прочие. Мы рассмотрим два типа подобных атак: с использованием инструкций сброса и без него.
Атаки с использованием инструкции сброса
Эти атаки пользуются памятью, разделяемой между двумя процессами. В основе атаки — большая разница во времени доступа к закэшированным и незакэшированным данным. Этот раздел про вариант Flush+Reload, алгоритм следующий:
Атакующий процесс выбирает интересующий его адрес из памяти, которую делит с процессом-жертвой.
Атакующий сбрасывает кэш-линию, к которой принадлежит выбранный адрес, с помощью специальной инструкции процессора.
Процесс-жертва выполняет какую-то работу.
Атакующий процесс замеряет время доступа к адресу. Если доступ быстрый, то процесс-жертва использовал этот адрес, если медленный, не использовал.

Также существует вариант Flush+Flush, который отличается только финальным этапом атаки, — вместо доступа к данным используется повторная инструкция сброса. Из-за разницы во времени исполнения сброса для закэшированных и незакэшированных данных получается тот же эффект.

Разница заметная, и ее легко обнаружить программно.
Тестовый стенд
Платформа Orange PI 5 со следующими характеристиками: Rockchip RK3588S с 4 ядрами Cortex-A76 с тактовой частотой до 2,4 ГГц и 4 ядрами Cortex-A55 с тактовой частотой до 1,8 ГГц. Тестирование проводилось на Cortex-A76.
Свойство non-aliasing также играет важную роль в атаке: виртуальные адреса у атакующего и жертвы могут быть разные, но из-за свойства когерентности изменения в кэш-памяти видны обоим процессам.
Атаки такого вида можно разделить на два класса. В кросс-процессорных атаках атакующий процесс и процесс-жертва исполняются на разных ядрах, а в локальных для процессора атаках — на одном ядре.
Кросс-процессорные атаки возможны, только если LLC инклюзивен. Если LLC-кэш неинклюзивный, то атакующий не сможет узнать, был ли доступ к памяти у процесса-жертвы, так как кэш-линия не будет находиться в LLC.
В случае локальных атак инклюзивность LLC не так важна: атака происходит в рамках одного ядра. Обычно в таких случаях атакующий процесс запускают одновременно на всех ядрах. Процент ошибок в локальных атаках выше: на этом же ядре могут исполняться другие процессы, способные вытеснить интересующие кэш-линии.
Разделяемые библиотеки
Разделяемые библиотеки — более распространенный вектор атак по побочным каналам, так как это самый частый случай разделяемой памяти между двумя несвязанными процессами. Чтобы сократить потребление памяти, неизменяемые данные, например код и константные глобальные объекты, физически разделяются между разными процессами. Разделяемая память появляется даже там, где, казалось, ее быть не должно.
Цель атак на разделяемые библиотеки не в секретах, которые просто редко являются частью библиотек. Атакующий пытается узнать аргумент, который передается в разделяемую библиотеку. Самый простой пример: криптографическая библиотека, которая расшифровывает входящие данные на входящем ключе. Атакуют именно микроархитектурное состояние, зависящее от входных секретов.
Более наглядный пример: оптимизация из реализации шифрования AES. Вдаваться в подробности алгоритма шифрования не будем. При инициализации библиотеки формируются lookup-таблицы, которые используют ключ и открытый/шифр-текст для индексации. Можно посмотреть, как это выглядит в реальной жизни, тут. О том, почему именно такая реализация — чуть ниже.
Атака развивается так:
Атакующий выгружает из кэша lookup-таблицу.
Атакующий просит жертву расшифровать известный атакующему тест на неизвестном ключе.
Атакующий смотрит, какие части lookup-таблицы были использованы, и восстанавливает ключ, зная шифр-текст.
Как митигировать: ограничить использование разделяемой памяти, если хотя бы один из процессов является недоверенным.
Атаки без инструкции сброса
Эти атаки пользуются тем, что количество кэш-линий в одном наборе кэш-памяти ограничено, а при коллизии адресов одна из кэш-линий будет вытеснена.
Evict+Probe
Атака требует разделяемой атакующим и жертвой памяти. Атакующий выбирает интересующий его адрес в разделяемой памяти и вычисляет набор в LLC, к которому этот адрес принадлежит. Этот набор называется eviction set.
Атакующий процесс заполняет eviction set своими приватными данными и ждет, пока выполняется процесс-жертва. После атакующий замеряет время доступа к своим данным, проверяя таким образом, вытеснены ли они из кэш-памяти. Если какой-то блок был вытеснен, процесс-жертва использовал адрес из разделяемой памяти.
Атака работает только на инклюзивных LLC-кэшах: если атакующий процесс вытеснил кэш-линию из LLC, то она также пропадает из приватного кэша другого CPU.

Такая атака сильно зависит от архитектуры кэш-памяти процессора. Обычно кэши уровня L2 и ниже делают PIPT: эту архитектуру проще реализовать, а задержки на уровнях < 1 уже не столь критичны.
Номер набора в кэшах PIPT вычисляется с помощью физического адреса, к которому процессы в пространстве пользователя чаще всего не имеют прямого доступа. Есть исключения, например, в Linux для этого используется /proc/$pid/pagemap. В некоторых конфигурациях, к примеру в Android, он доступен непривилегированным пользователям.
Prime+Probe
Данная атака привлекательна тем, что не требует разделяемой процессами памяти. У нее две фазы: обучение и получение информации.
Обучается атакующий процесс так: заполняет всю кэш-память своими приватными данными и подает процессу-жертве какой-то вход. При работе процесса-жертвы часть приватных данных вытесняется. Атакующий процесс обходит свои данные, детектирует, какие из них были вытеснены, замеряя время доступа.
Процесс повторяется несколько раз: строится база данных, в которой каждый вход для жертвы сопоставляется с рядом наборов с вытесненными линиями.
Во второй фазе атакующий вновь заполняет весь кэш и следит, какие данные вытесняются. С полученной на этапе обучения базой данных можно сделать вывод, какой вход обрабатывал процесс-жертва в данный момент.
Атака может быть как локальной, так и кросс-процессорной. В режиме cross-core атакующий пытается вытеснить данные из LLC-кэша, пользуясь его инклюзивностью. В случае локальной атаки атакующий процесс создает потоки на каждом из процессорных ядер и проводит атаку по приватным для процессора кэшам. Сам смысл атаки остается таким же, однако данные вытесняются из кэшей более высокого уровня.
Как митигировать: для кросс-процессорных атак использовать Autolock. От локальных атак защищает отсутствие в пространстве пользователя достаточно точного источника времени, способного измерять время в тактах.
Митигации
Autolock
Какие атаки митигирует: Evict+Reload-атаки, кросс-платформенные Prime+Probe-атаки.
Исследователи безопасности обнаружили необычное поведение в кэш-памяти процессоров семейства Cortex-A производства ARM. Оно сильно мешает проведению Evict+Reload и Prime+Probe-атак, а также всех иных, в которых не участвует инструкция сброса.
Напомню, атака Evict+Reload вытесняет все кэш-линии из eviction set своими приватными данными и ждет, что процесс-жертва вытеснит уже их в процессе работы. Механизм работает строго на инклюзивных кэшах.
Инженеры ARM решили внести информацию об инклюзивности каждой кэш-линии в железо. При коллизии адресов из кэш-памяти вытесняются только неинклюзивные на данный момент линии: то есть копии этих кэш-линий не присутствуют на уровнях выше.
Скорее всего, инженеры думали о производительности, так как сброс инклюзивной линии для нее сильно дороже. Однако механизм также противодействует атакам на кэш-память.
Рассмотрим, как поведет себя кэш-память с такой модификацией в случае атаки Prime+Probe.



При дальнейших попытках вытеснить данные из кэша всегда будут выбираться линии с битом инклюзивности, равным 0, то есть данные атакующего. Важно отметить, что такие линии всегда найдутся из-за разницы в размерах кэшей.
Prime+Probe- и Evict+Reload-атаки просто не работают на процессорах семейства Cortex-A. Остается вариант с атакой на локальном процессоре: основываясь на собственных экспериментах, скажу, что они довольно «шумные», а это сильно усложняет их применимость.
Не использовать разделяемую память
Какие атаки митигирует: Разделяемые библиотеки.
Отсутствие разделяемой памяти между атакующим процессом и процессом-жертвой сильно усложняет атаки по побочным каналам. У атакующего все еще есть возможность по некоторым косвенным признакам узнать информацию об активности соседнего процесса, но такие атаки очень шумные, что делает их применение более сложным. О таких атаках можно почитать, например, тут или тут.
В операционных системах основным источником разделяемой памяти между процессами являются разделяемые библиотеки. Их код и read-only-данные обычно разделяются между всеми процессами, которые их используют. В редких случаях разделяемая память может быть использована в качестве IPC.
В KasperskyOS мы контролируем оба этих фактора. Все процессы в системе делятся на домены безопасности. Для каждого домена есть свой отдельный blob_container. Тем самым только процессы одного уровня могут иметь одинаковые разделяемые библиотеки.
Пару слов о blob_container в KasperskyOS
Как вам, скорее всего, известно, это микроядерная операционная система, поэтому загрузка приложений полностью вынесена из пространства ядра. Ядро загружает лишь самое первое приложение. Сделано так из соображений минимизации кода TCB (Trusted Computing Base), что урезает поверхность атаки на ядро.
За часть загрузки, связанную с разбором ELF-файлов, отвечает отдельный компонент — blob_container. Этот компонент также отвечает за загрузку динамических библиотек, если таковые присутствуют в файле.
Установить канал связи через разделяемую память можно только при явном разрешении в политике безопасности.
FlushTime
Какие атаки митигирует: Атаки с использованием инструкции сброса.
Митигация сильно затрудняет атаки, которые используют инструкцию сброса. Авторы митигации сделали вывод, что основной фактор успеха атак — таймер высокого разрешения, доступный в пространстве пользователя: регистры CNTPCT_EL0 или CNTVCT_EL0 в случае ARMv8. ARMv8 позволяет конфигурировать их доступность из пространства пользователя.
Если доступ был запрещен, то при попытке чтения регистра таймера из пространства пользователя будет выброшено специальное исключение, которое ядро операционной системы может распознать. Механизм запрета применим к инструкции сброса кэш-линии, и так же можно отловить исключение использования специальной инструкции.

Гранулярностью вычисления срока выбрали переключение контекста задач. В ходе исследования авторы выяснили, что, например, в случае операционной системы Linux стоит занижать частоту таймера на период 1000 переключений контекста.
После сброса кэш-линии таймер будет в низкочастотном режиме, что не позволит с высокой точностью вычислять время доступа к памяти и сильно затруднит задачу обнаружения промаха по кэш-памяти.
Отмечу, что описанный выше механизм не попал в upstream ядра, а был реализован в форке для показа жизнеспособности идеи.
Данная митигация неоптимальна в рамках KasperskyOS: сброс кэш-памяти может контролироваться политиками безопасности.
Модификация потенциально опасного кода
Какие атаки митигирует: потенциально все атаки на кэш-память.
Все атаки на кэш-память в общем случае используют один и тот же эффект. Если процесс имеет блок кода, который по-разному влияет на микроархитектурное состояние в зависимости от секрета, этот процесс уязвим. Как было показано ранее, пример такого подхода — различные оптимизации алгоритмов шифрования. Идея митигации — изменить такой код на версию, в которой не происходит проблем. Для примера можно посмотреть на патч в PuTTY.
To avoid the obvious side-channel leakage of a literal table lookup, we read the whole table every time, mp_selecting the right value into the multiplication input. This isn't as slow as it sounds when the alternative is four entire modular multiplications! In my testing, this commit speeds up large modpows by a factor of just over 1.5, and it still gets a clean pass from 'testsc'
Вместо точечного чтения, зависящего от секрета, читается вся таблица. Она оказывается в кэш-памяти целиком, атакующий не может узнать, какой элемент таблицы действительно использовался.
К сожалению, средств статического анализа, которые могли бы такое обнаруживать автоматически, не нашлось. В целом кажется, что статический анализ тут бессилен, полагаться можно только на ручной аудит и код-ревью, усиленный с помощью LLM.
Применимость атак в KasperskyOS
Rowhammer
Атака сильно зависит от чипа DRAM, что в общем случае делает ее непереносимой. Ряд исследований (одно, еще одно) подобных атак на платформах ARMv8 показывает, что используются следующие механизмы:
Предоставление информации о трансляции виртуальных адресов в физический в Linux при помощи /proc/$pid/pagemap.
Аллокации некэшируемой памяти.
Детали работы аллокатора физических страниц для контроля того, куда уходят атакуемые физические страницы.
Ни один из этих механизмов недоступен в рамках KasperskyOS, но нет гарантии, что они не появятся в будущем или не будет придумано более изощренного способа атаки. На текущий момент атака неприменима, но в будущем возможна.
Насколько мне известно, программных митигаций не существует, но без механизма сброса кэш-памяти атака на ARMv8 будет сильно затруднена.
Разделяемые библиотеки
Атака теоретически применима в KasperskyOS. В KasperskyOS данная атака ограничена уровнем blob_container-а, который является единицей разделения библиотек. Дело в том, что, в отличие от традиционных решений, в KasperskyOS не единственный корень поиска библиотек, система конфигурирует такое количество доменов безопасности с уникальным blob_container-ом, как того требуют цели безопасности.
Evict+Reload
В KasperskyOS атака неприменима: не было найдено механизма для трансляции виртуального адреса в физический из пространства пользователя.
Prime+Probe
Атака не применима к KasperskyOS. При локальном исследовании не удалось воспроизвести на тестовом стенде ни локальные, ни кросс-платформенные атаки.
Cross-core-атака не получилась, потому что тестовый стенд основан на процессорах типа Cortex-A, в которых существует Autolock-митигация.
Локальная атака не вышла из-за отсутствия достаточно точного измерения источника времени. Например, для процессоров типа Cortex-A55 попадание в L2-кэш занимает 8 тактов, а попадание в L3 — 13 тактов. В KasperskyOS самый высокочастотный источник времени — generic timer, который может работать на частоте, отличной от частоты процессора.
На тестовом стенде частота таймера, которую можно получить из регистра CNTFRQ_EL0, —24МГц. Минимальная частота процессора Cortex-A55 — 408МГц, основываясь на выводе lscpu. Посчитать единицы времени в тактах невозможно.
Атаки на операционной системе Linux используют возможности модуля PMU. Если к нему разрешен доступ из пространства пользователя, при его помощи можно получать время в тактах. В KasperskyOS доступ к модулю PMU выключен для пространства пользователя.
Итоги
Как было показано, разрешение использовать инструкции по работе с кэш-памятью может серьезно повлиять на безопасность системы. Однако их использование в пространстве пользователя может дать сильный прирост производительности, особенно если речь идет про драйверы или JIT-компиляторы.
Для митигации таких атак нужна комплексная работа, как на уровне исходного кода, так и на уровне архитектуры системы. В целом для атак по побочным каналам может быть использовано почти что угодно, что разделяется между атакующим и атакуемым. Если минимизировать объемы разделяемых ресурсов между доменами, то такие атаки становятся сложнее.