В последние годы стало появляться большое количество сообщений о всякого рода уязвимостях в процессорах компании Intel. Самыми известными из них являются Spectre и Meltdown, основанные на ошибках в реализации спекулятивного исполнения команд. В июне 2020 года появилось сообщение о новой уязвимости, носящей название Crosstalk.

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

Спекулятивные вычисления

Спекулятивное исполнение команд процессором является одним из аппаратных способов выявления параллелизма на уровне команд. Вычисления проводятся одновременно для нескольких путей исполнения программы. Простейшим примером является спекулятивное вычисление двух веток при условном переходе.

Конвейер

Конвейер процессора представляет собой технологию, позволяющую распараллелить инструкции процессора на более простые, что позволяет производить вычисления с большей скоростью. На рисунке показано, как происходит исполнение инструкций в четырех стадийном конвейере. Для примера, на третьем такте происходит исполнение зеленой инструкции, декодирование фиолетовой и подтягивание новой синей инструкции. Без конвейера такой набор из четырех инструкций занял бы 16 тактов процессорного времени. В нашем случае, команды исполнились за 8 тактов.

Как происходит межъядерное взаимодействие?

Существует набор инструкций процессора Интел семейства x86 с нетривиальным поведением. Такие инструкции состоят из нескольких операций, называемых микрокодом. Исследователи из Vrije Universiteit Amsterdam провели ряд экспериментов, в которых изучалась работа инструкций процессора с разными наборами аргументов. Оказывается, что в ряде случаев такой микрокод осуществляет операции чтения-записи вне ядра через внутренние шины процессора в регистры MDS (Model-Specific-Registers) с помощью операций RDMSR и WRMSR. Эти операции являются привилегированными и могут исполняться только операционной системой. Для userspace примерами таких инструкций являются CPUID, RDRAND и RDSEED.

Промежуточный буфер сохраняет результаты ранее выполненных инструкций, такие как сгенерированные DRNG случайные числа, хэши состояния bootguard и другие ценные данные. Чтение из промежуточного буфера с помощью Crosstalk позволяет получать доступ к данным, к которым обращался пользователь, и которые остались в этом буфере после выполнения таких запросов, как например RDRAND и RDSEED.

RDRAND и RDSEED

Инструкция RDRAND возвращает случайные числа, полученные от digital random number generator (DRNG), и может быть вызвана из пользовательского пространства. DRNG выводит случайные начальные состояния и передает их генератору случайных битов, который заполняет глобальную очередь случайных чисел. Инструкция RDSEED обеспечивает доступ к более качественному источнику энтропии, т.к. предназначена для программных RNG.

Внутренние буферы процессора

Забегая немного назад в списке уязвимостей, стоит отметить RIDL, которая позволяет создавать утечки информации из разных источников, таких как кэши и буферы процессора: Line Fill Buffer, Load Ports, Store Buffer.

Line Fill Buffer (LFB) используется для отслеживания кэш миссов L1 Cache (невыполненных запросов памяти) и передачи кэш-линий за пределами L1 Cache. Например, при кэш миссе, вместо блокировки кэша, операция помещается в LFB и обрабатывается асинхронно. Это позволяет кэшу обслуживать другие запросы. Промежуточный буфер получает данные от ядра из LFB.

Store Buffer отслеживает запросы на запись данных.

Load Ports используются конвейером процессора при загрузке данных из памяти или I/O операций. Когда выполняется микрокод загрузки, данные сначала сохраняются в Load Ports перед передачей в регистры.

Детектирование Crosstalk

Детектирование Crosstalk состоит из двух стадий. Сначала проверяются различные инструкции, делающие запросы вне ядра с разными операндами. Затем анализируются данные, полученные от такого рода запросов, чтобы понять, как устроено взаимодействие между LFB, расположенном на ядре, и внешним разделяемым буфером. Разные инструкции записывают данные в разделяемый буфер с разным размером отступа. На другом ядре происходит чтение из этого буфера и наблюдение за изменениями, вносимыми первым ядром. Таким образом, злоумышленник может сгенерировать набор инструкций, позволяющий читать почти все инструкции, поступающие в промежуточный буфер от другого ядра.

FLUSH + RELOAD

inline int probe(char *adrs) {
  volatile unsigned long time;

  asm __volatile__ (
    "  mfence             \n"
    "  lfence             \n"
    "  rdtsc              \n"
    "  lfence             \n"
    "  movl %%eax, %%esi  \n"
    "  movl (%1), %%eax   \n"
    "  lfence             \n"
    "  rdtsc              \n"
    "  subl %%esi, %%eax  \n"
    "  clflush 0(%1)      \n"
    : "=a" (time)
    : "c" (adrs)
    :  "%esi", "%edx");
  return time;
}

В качестве примера рассмотрим RIDL атаку с использованием LFB, выполняемую в четыре этапа. Сначала злоумышленник создает массив FLUSH + RELOAD, содержащий одно значение для каждой строки кэша (обычно байт) и выполняет операцию FLUSH, чтобы гарантировать, что ни одна из этих строк не находится в кэше. Затем злоумышленник предлагает программе-жертве прочитать или записать секретные данные или обеспечить удаление таких данных из кэша. В любом случае, процессор перемещает данные в LFB. Затем злоумышленник выполняет загрузку данных (операцию load), вызывающую исключение или pagefault. При этом, такая операция считается успешной, данные сохраняются в LFB. Затем спекулятивно исполняемый код злоумышленника использует данные, соответствующие индексу в массиве FLUSH + RELOAD. Соответствующая строка кэша будет загружена в кэш конвейером, когда он выполнит спекулятивный код. Наконец, загрузив каждый элемент массива и определив время загрузки, злоумышленник может определить, какой из них был в кеше. Индекс в кэше - это секретные данные, полученный из LFB.

CPUID

pid_t pid = fork();
if (pid == 0) {
    while (1)
        asm volatile(
            "mov %0, %%eax\n"
            "cpuid\n"
            ::"r"(CPUID_LEAF):"eax","ebx","ecx","edx");
}

for(size_t offset = BEGIN_OFFSET; offset < BEGIN_OFFSET + 4; ++offset) {
    // ...
    for(size_t i(0); i < ITERS; ++i) {
        flush(reloadbuffer);
        tsx_leak_read_normal(leak + offset, reloadbuffer);
        reload(reloadbuffer, results);
    }
}

На представленном листинге показано, как осуществляется запрос на примере CPUID. Эта команда позволяет получить информацию о процессоре. Такого рода запросы называются MDS. К их числу относится упомянутый ранее RIDL. Запросы проводятся с разным смещением в разделяемом буфере. Смещение вызывает ошибку при чтении страницы, так как читаемый вектор захватывает границы страницы. Затем при помощи FLUSH + RELOAD можно получить данные, прочитанные во время выполнения инструкции. Таким образом, CPUID вызывает 4 запроса вне ядра, что говорит об успешной демонстрации CROSSTALK. В следующей таблице представлены результаты различных операций, реализуемых CROSSTALK

Замедление работы процессора

Одним из примеров атаки может служить замедление работы при запросах определенного рода ресурсов. Рассмотрим инструкцию RDSEED. Объем доступной энтропии всегда ограничен, причем RDSEED возвращает 0, если нет доступной энтропии. Неуспешный вызов RDSEED не перезаписывает содержимое промежуточного буфера. Таким образом, злоумышленник может потреблять доступную энтропию, самостоятельно выполняя запросы RDRAND и RDSEED, в то время как ядро-жертва не сможет получить достаточный объем энтропии для успешного завершения вызова RDSEED. С помощью такого рода запросов можно читать данные, записанные пользователем в разделяемый буфер. Когда запрос жертвы все же вернет положительный результат, данные в разделяемом буфере перезапишутся. Но в то же время, злоумышленник может уже прочитать данные, до завершения работы вызовов FLUSH + RELOAD.

Виртуальные машины

Если злоумышленники имеют возможность писать код только внутри виртуальной машины, то операции, позволяющие получать доступ к промежуточному буферу, ограничены. Например, обычно виртуальные машины запрещают пользователю выполнять вызов CPUID, чтобы не позволять пользователю получать информацию о возможностях виртуальной машины. Тем не менее, инструкции RDRAND и RDSEED могут быть исполнены из пространства пользователя, что создает уязвимость и для виртуальных машин. Примером может стать составление запросов гипервизору, а затем чтение из промежуточного буфера в LFB. Даже если процессор оснащен защитой от MDS атак, злоумышленник может получать содержимое разделяемого буфера из соседнего потока (hyperthread), раскрывая данные жертвы, работающей на другом ядре.

Устранение уязвимости

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

Выводы

Crosstalk представляет собой уязвимость нового рода, позволяющую злоумышленникам получать доступ к данным из промежуточного буфера, разделяемого несколькими ядрами. Предыдущие способы борьбы с MDS уязвимостями не позволяют бороться с такого вида атаками (а в ряде случаев, ухудшают ситуацию). Решение проблемы позволяет полностью ограничить доступ к данным, записанным другим ядром в промежуточный буфер, но создает дополнительные накладные расходы из-за блокирования буфера. Несмотря на то, что большинство современных процессоров Интел подвержены межъядерным атакам, компании не известно ни одного примера атаки за пределами лаборатории. В то же время, на серверных процессорах высокого уровня обеспечивается защита от такого рода угроз, и некоторые наиболее современные процессоры не подвержены атакам MDS.