Come mitigazione contro i kernel exploit, Linux include vari meccanismi di sicurezza. Alcuni, come NX nello userland, vivono a livello hardware è quindi la stessa conoscenza torna utile anche nei kernel exploit Windows.

Qui ci concentreremo sulle protezioni specifiche del kernel. Meccanismi già noti, come Stack Canary, esistono anche nei device driver, ma non hanno particolarità tali da meritare una trattazione separata.

Per i parametri di boot del kernel, la documentazione ufficiale è molto utile.

SMEP (Supervisor Mode Execution Prevention)

Fra le protezioni del kernel, le più rappresentative sono SMEP e SMAP.
SMEP vieta al codice in esecuzione in kernel mode di saltare improvvisamente a codice che si trova in userland. Come concetto ricorda NX.

SMEP è una mitigazione, non una difesa assoluta. Immaginiamo che una vulnerabilità in kernel space consenta a un attaccante di prendere controllo di RIP. Se SMEP è disabilitato, diventa possibile eseguire direttamente uno shellcode preparato in userland:

1
2
3
4
5
char *shellcode = mmap(NULL, 0x1000, PROT_READ|PROT_WRITE|PROT_EXECUTE,
MAP_ANONYMOUS | MAP_PRIVATE, -1, 0);
memcpy(shellcode, SHELLCODE, sizeof(SHELLCODE));

control_rip(shellcode); // RIP = shellcode

Se SMEP è attivo, invece, il tentativo di eseguire quello shellcode genera un kernel panic. Di conseguenza, anche in presenza di controllo di RIP, diventa più difficile trasformare il bug in una LPE.

Lupetto

Ma allora cosa bisogna eseguire come shellcode in kernel space?
I metodi per ottenere l'elevazione di privilegi li vedremo in un altro capitolo.

SMEP può essere abilitato passando un’opzione a qemu. Se l’opzione -cpu contiene +smep, SMEP è attivo:

1
-cpu kvm64,+smep

Dall’interno della VM si può controllare anche tramite /proc/cpuinfo:

1
$ cat /proc/cpuinfo | grep smep

SMEP è una protezione hardware. Si abilità impostando il bit 21 del registro CR4.

SMAP (Supervisor Mode Access Prevention)

Che lo userland non possa leggere o scrivere la memoria del kernel è ovvio. Meno intuitivo è il contrario: esiste una protezione chiamata SMAP (Supervisor Mode Access Prevention) che impedisce al kernel di leggere e scrivere arbitrariamente la memoria userland. Per accedere ai dati dello userland, il kernel dovrebbe usare funzioni dedicate come copy_from_user e copy_to_user.
Perché limitare il kernel, che ha privilegi superiori, nell’accesso a dati userland?

Non conosco il dettaglio storico, ma i vantaggi principali di SMAP sono almeno due.

Il primo è che ostacola gli stack pivot.
Nell’esempio precedente, con SMEP attivo non è più possibile eseguire shellcode userland. Tuttavia il kernel Linux contiene una quantità enorme di codice macchina, quindi gadget come il seguente esistono quasi certamente:

1
mov esp, 0x12345678; ret;

Qualunque sia il valore scritto in ESP, dopo questo gadget RSP verrà cambiato di conseguenza[1]. E poiché indirizzi bassi come questo possono essere riservati via mmap dallo userland, anche con SMEP attivo l’attaccante può ancora preparare una ROP chain in userland e pivotarvi dentro:

1
2
3
4
5
6
7
8
void *p = mmap(0x12340000, 0x10000, ...);
unsigned long *chain = (unsigned long*)(p + 0x5678);
*chain++ = rop_pop_rdi;
*chain++ = 0;
*chain++ = ...;
...

control_rip(rop_mov_esp_12345678h);

Se però SMAP è attivo, quei dati mappati in userland non sono leggibili dal kernel. Di conseguenza la ret del pivot fara crashare il kernel. Quindi SMEP più SMAP mitigano efficacemente gli attacchi ROP che si appoggiano a memoria userland.

Il secondo vantaggio di SMAP è che aiuta a prevenire bug molto facili da commettere in programmazione kernel.
Immagina che un driver contenga codice di questo tipo (non importa, per ora, capire ogni dettaglio della firma):

1
2
3
4
5
6
7
8
9
10
char buffer[0x10];

static long mydevice_ioctl(struct file *filp, unsigned int cmd, unsigned long arg) {
if (cmd == 0xdead) {
memcpy(buffer, arg, 0x10);
} else if (cmd == 0xcafe) {
memcpy(arg, buffer, 0x10);
}
return
}

Si capisce abbastanza chiaramente che memcpy sta leggendo e scrivendo dati da e verso una variabile globale chiamata buffer.

Dal punto di vista dello userland, il modulo si usa così:

1
2
3
4
5
6
7
8
9
int fd = open("/dev/mydevice", O_RDWR);

char src[0x10] = "Hello, World!";
char dst[0x10];

ioctl(fd, 0xdead, src);
ioctl(fd, 0xcafe, dst);

printf("%s\n", dst); // --> Hello, World!

Se vieni dalla programmazione userland, qui non sembra esserci nulla di strano: la dimensione della memcpy è fissa e sembra tutto innocuo.

Ma se SMAP fosse disattivato, sarebbe possibile anche una chiamata come:

1
ioctl(fd, 0xdead, 0xffffffffdeadbeef);

Questo è un indirizzo invalido in userland, ma supponiamo che nel kernel corrisponda a un’area contenente dati sensibili. Il driver finirebbe per eseguire:

1
memcpy(buffer, 0xffffffffdeadbeef, 0x10);

e leggerebbe così dati segreti del kernel. Se si usa memcpy direttamente su un puntatore fornito dallo userland senza controlli, il risultato e, di fatto, la possibilità di leggere o scrivere indirizzi arbitrari in kernel space.
Per chi non ha familiarità con la programmazione kernel è un bug molto poco intuitivo, ma l’impatto è enorme: porta direttamente a primitive AAR/AAW. Anche per questo SMAP è molto utile.

Come SMEP, anche SMAP si può abilitare via opzione qemu:

1
-cpu kvm64,+smap

e verificare via /proc/cpuinfo:

1
$ cat /proc/cpuinfo | grep smap

Anche SMAP è una protezione hardware. Si abilità impostando il bit 22 di CR4.

Lupetto

Sulle CPU Intel esistono le istruzioni STAC e CLAC, che rispettivamente impostano o azzerano il flag EFLAGS.AC (Alignment Check). Finché AC è impostato, l'effetto di SMAP viene sospeso.

KASLR / FGKASLR

In userland esiste ASLR (Address Space Layout Randomization), che randomizza gli indirizzi in memoria. In modo analogo, Linux offre KASLR (Kernel ASLR), che randomizza gli indirizzi delle sezioni di codice e dati del kernel e dei driver.
Il kernel, una volta caricato, non viene più spostato, quindi KASLR agisce una sola volta durante il boot. Se si riesce a leakare l’indirizzo di una qualunque funzione o dato del kernel, il base address si può ricavare.

Dal 2020 esiste anche FGKASLR (Function Granular KASLR), una variante più forte che randomizza l’indirizzo di ogni singola funzione del kernel. Nel 2022 risulta ancora disabilitata di default, ma il principio è interessante: anche se si leakasse l’indirizzo di una funzione del kernel, da quello non si potrebbe più ricavare il base address.
FGKASLR, però, non randomizza i dati, quindi se si riesce a leakare l’indirizzo di una variabile in .data si può comunque recuperare il base address. Da quel base address non si può più ottenere direttamente l’indirizzo delle funzioni, ma esistono vettori di attacco particolari in cui resta comunque utile.

Attenzione al fatto che gli indirizzi del kernel sono globali per tutto il sistema. Quindi, anche se un certo driver fosse non sfruttabile grazie a KASLR, se un altro driver leakasse indirizzi del kernel l’intera macchina diventerebbe nuovamente attaccabile.

KASLR si può disattivare tramite i parametri di boot del kernel. Se in -append compare nokaslr, KASLR è disattivato:

1
-append "... nokaslr ..."

KPTI (Kernel Page-Table Isolation)

Nel 2018 è stato scoperto un side-channel molto grave, Meltdown, che permetteva di leggere memoria del kernel con privilegi utente è quindi, per esempio, di aggirare KASLR. Per mitigarlo, i kernel Linux moderni abilitano KPTI (Kernel Page-Table Isolation), noto in passato anche come KAISER.

Come sappiamo, la traduzione da indirizzi virtuali a fisici passa attraverso le page table. KPTI consiste nel separare le page table usate in user mode e quelle usate in kernel mode[2]. KPTI esiste per fermare Meltdown, quindi normalmente non è un ostacolo diretto per i kernel exploit tradizionali. Diventa invece rilevante quando, per esempio, si costruisce una ROP chain in kernel space e poi si deve tornare in userland. La procedura precisa la vedremo più avanti nel capitolo sul Kernel ROP.

KPTI si controlla tramite i parametri di boot. Se in -append compare pti=on, KPTI è attivo; se compare pti=off o nopti, è disabilitato:

1
-append "... pti=on ..."

Dall’interno della VM, lo si può verificare leggendo /sys/devices/system/cpu/vulnerabilities/meltdown. Se compare:

1
2
# cat /sys/devices/system/cpu/vulnerabilities/meltdown
Mitigation: PTI

allora KPTI è attivo. Se è disattivato, comparira Vulnerable.

Dal momento che KPTI si basa sul cambio di page table, si può cambiare spazio utente/kernel manipolando CR3. Su Linux, fare OR di 0x1000 a CR3 (cioè cambiare il PDBR) consente di passare dal contesto kernel a quello userland. Questa logica si trova in swapgs_restore_regs_and_return_to_usermode, ma la analizzeremo quando scriveremo exploit reali.

KADR (Kernel Address Display Restriction)

Su Linux, nomi e indirizzi dei simboli del kernel si possono leggere da /proc/kallsyms. Inoltre alcuni driver stampano informazioni di debug tramite printk, consultabili dall’utente con strumenti come dmesg.
Per impedire che questi meccanismi leakino indirizzi di funzioni, dati o heap del kernel, Linux include una protezione che, anche se non ha un nome ufficiale, in questa referenza viene chiamata KADR (Kernel Address Display Restriction), nome che adotteremo anche qui.

Il comportamento dipende dal valore di /proc/sys/kernel/kptr_restrict.
Se kptr_restrict = 0, gli indirizzi vengono mostrati senza limitazioni.
Se kptr_restrict = 1, gli indirizzi sono visibili solo agli utenti dotati della capability CAP_SYSLOG.
Se kptr_restrict = 2, gli indirizzi del kernel vengono nascosti persino agli utenti privilegiati.
Se KADR è disattivato, non serve più ottenere un address leak, quindi verificarlo subito può rendere l’exploit molto più semplice.


Esegui le seguenti verifiche sul kernel di LK01. (Parti dal presupposto di avere già una shell root, come nell'esercizio precedente.)
(1) Leggi run.sh e controlla se KASLR, KPTI, SMAP e SMEP sono attivi.
(2) Avvia la VM con le opzioni che attivano sia SMAP sia SMEP e verifica in /proc/cpuinfo che siano davvero attivi. Dopo il test, ricordati di disattivarli di nuovo.
(3) L'indirizzo che compare per primo in head /proc/kallsyms è il base address del kernel. Se KASLR è disattivato, verifica quale valore assume. (Suggerimento: fai attenzione a KADR.)

  1. Su x64, il risultato di un’operazione su registri a 32 bit viene esteso al corrispondente registro a 64 bit. ↩︎

  2. L’unica area condivisa fra user mode e kernel mode resta quella necessaria a invocare le system call. ↩︎