In LK01 abbiamo già coperto quasi tutte le basi necessarie per il kernel exploit. Da qui in avanti ci concentreremo quindi su tecniche più specifiche del kernel o su vulnerabilità legate a funzionalità particolari di Linux.
Nel capitolo LK02 (Angus) vedremo come sfruttare una NULL Pointer Dereference in kernel space. Per prima cosa scarica LK02.
Nota sul tipo di vulnerabilità trattato qui
Se guardi le opzioni di avvio qemu di LK02, noterai che SMAP è disattivato. La tecnica di exploit che vedremo per questa NULL pointer dereference dipende proprio dal fatto che SMAP non sia attivo.
Inoltre, prova a eseguire sul target:
1 | $ cat /proc/sys/vm/mmap_min_addr |
mmap_min_addr è una variabile del kernel Linux che, come dice il nome, impone un limite inferiore agli indirizzi mappabili dallo userland tramite mmap. Di default il valore è diverso da zero, ma qui è stato impostato a 0. Questa mitigazione è stata introdotta a partire da Linux 2.6.23 proprio per rendere difficili da sfruttare vulnerabilità di questo tipo.
In altre parole, il capitolo assume che si possa aggirare o disattivare le difese moderne contro la NULL pointer dereference. Se ti interessano solo attacchi applicabili ai kernel Linux recenti con tutte le mitigazioni standard, puoi anche saltarlo.
Verifica della vulnerabilità
Partiamo dal sorgente di LK02, che si trova in src/angus.c.
ioctl
Rispetto a LK01, la differenza principale è che non ci sono handler per read e write; al loro posto il modulo implementa un handler per la system call ioctl.
Quando lo userland chiama ioctl su un file descriptor, il kernel inoltra la richiesta al relativo handler del driver.
ioctl prende, oltre al file descriptor, due argomenti: request e argp.
1 | ioctl(fd, request, argp); |
request è un codice che identifica l’operazione da eseguire sul device; i codici sono definiti liberamente dal driver, quindi vanno sempre cercati nel sorgente.
argp, di solito, contiene un puntatore a dati userland che il modulo legge con copy_from_user.
Anche qui funziona così: il modulo si aspetta di ricevere una struttura request_t.
1 | typedef struct { |
Subito dopo si nota che il comportamento cambia in base al codice cmd:
1 | switch (cmd) { |
Prima di analizzare ogni caso, serve capire che cosa sia private_data.
Struttura file
Quando lo userland interagisce con un driver usa un file descriptor; lato kernel, quel descriptor viene rappresentato da una struct file.
La struttura contiene informazioni specifiche del file, per esempio l’offset di lettura impostato da lseek[1], ma include anche un campo che il modulo può usare liberamente:
1 | struct file { |
Il modulo può usarlo come preferisce, purche si occupi correttamente di allocare è liberare i dati. In questo challenge viene usato per salvare una struttura personalizzata chiamata XorCipher.
1 | static int module_open(struct inode *inode, struct file *filp) { |

Se anche LK01-4 (Holstein v4) avesse tenuto i dati in private_data, la race non si sarebbe verificata.
Panoramica del programma
Il modulo implementa una specie di cifratura XOR nel kernel.
Si controlla via ioctl e mette a disposizione sei richieste:
1 |
Con CMD_INIT si alloca e inizializza in private_data una struttura XorCipher:
1 | typedef struct { |
La struttura contiene:
- un puntatore alla chiave (
key) è la sua lunghezza (keylen) - un puntatore ai dati (
data) è la loro lunghezza (datalen)
Con CMD_SETKEY si copia in kernel space la chiave fornita dallo userland. Se una chiave era già presente, viene liberata prima.
1 | case CMD_SETKEY: |
CMD_SETDATA fa la stessa cosa per il payload da cifrare o decifrare:
1 | case CMD_SETDATA: |
Con CMD_GETDATA si possono copiare i dati dallo spazio kernel allo userland:
1 | case CMD_GETDATA: |
Infine CMD_ENCRYPT e CMD_DECRYPT chiamano la stessa funzione xor, perché nel cifrario XOR cifrare e decifrare sono la stessa operazione:
1 | long xor(XorCipher *ctx) { |
Individuare la vulnerabilità
Nel modulo non ci sono buffer overflow o Use-after-Free evidenti. Se lo leggi con attenzione, però, c’è una NULL pointer dereference nel percorso di cifratura/decifratura.
All’inizio dell’handler ioctl, private_data viene castato a XorCipher *:
1 | ctx = (XorCipher*)filp->private_data; |
In comandi come CMD_SETKEY o CMD_SETDATA si controlla correttamente che ctx non sia NULL:
1 | if (!ctx) return -EINVAL; |
Ma CMD_GETDATA, CMD_ENCRYPT e CMD_DECRYPT non eseguono questo controllo:
1 | long xor(XorCipher *ctx) { |
Quindi, se XorCipher non è stato inizializzato, il modulo dereferenziera un puntatore NULL.
Verificare il bug
Partiamo da un uso corretto del modulo. Conviene preparare piccole wrapper function per ciascun comando:
1 | int angus_init(void) { |
Per esempio, possiamo cifrare "Hello, World!" con la chiave "ABC123" e poi decifrarlo di nuovo:
1 | int main() { |
Se tutto funziona, i dati vengono cifrati e poi recuperati correttamente:
Ora proviamo invece a cifrare senza avere mai inizializzato XorCipher:
1 | int main() { |
Eseguendo il programma, dovresti finire in un kernel panic:
Nel messaggio compare qualcosa come kernel NULL pointer dereference, address: 0000000000000008, confermando che il kernel ha davvero dereferenziato un puntatore nullo.
Le NULL pointer dereference compaiono spesso anche in userland, ma di solito non sono sfruttabili. Qui, invece, vedremo perché possono diventarlo.
Memoria virtuale e mmap_min_addr
Secondo il modello di memoria di Linux, diverse fasce di indirizzi virtuali hanno ruoli diversi. Per esempio, da 0000000000000000 a 00007fffffffffff lo spazio è disponibile allo userland; da ffffffff80000000 a ffffffff9fffffff troviamo invece memoria del kernel mappata a partire dalla RAM fisica.

Su Linux gli indirizzi a 48 bit vengono estesi con segno a 64 bit. Per questo l'intervallo da 0x800000000000 a 0xffff7fffffffffff è invalido e viene chiamato non-canonical.
Dato che l’intervallo 0000000000000000 - 00007fffffffffff appartiene allo userland, se riusciamo a mappare davvero l’indirizzo 0, una NULL pointer dereference non provoca più un crash immediato: il kernel finisce per leggere o scrivere dati che l’attaccante ha preparato a quell’indirizzo. Questo ovviamente è possibile solo se SMAP è disabilitato.
Con mmap, il primo argomento a NULL significa normalmente “scegli tu l’indirizzo”. Se invece si usa MAP_FIXED, il mapping viene fatto esattamente all’indirizzo richiesto o fallisce. Così si può tentare di mappare la pagina a indirizzo zero. (Con KPTI attivo, conviene anche aggiungere MAP_POPULATE.)
1 | mmap(0, 0x1000, PROT_READ|PROT_WRITE, |
Sul challenge questo funziona, ma sulla maggior parte delle normali macchine Linux fallirà. Il motivo è proprio mmap_min_addr:
1 | $ cat /proc/sys/vm/mmap_min_addr |
Lo userland non può mappare indirizzi più piccoli di questo valore. In condizioni normali, quindi, una NULL pointer dereference risulta non sfruttabile. Qui invece mmap_min_addr vale 0, quindi l’attacco è possibile.
Elevazione di privilegi
Dal momento che il modulo dereferenzia un puntatore NULL trattandolo come XorCipher, l’attaccante può preparare una finta struttura XorCipher all’indirizzo 0:
1 | typedef struct { |
Manipolando data e datalen, CMD_GETDATA permette di leggere dati da un indirizzo arbitrario. Impostando opportunamente anche key e keylen, si può arrivare a scrivere su un indirizzo arbitrario.
In altre parole, la vulnerabilità consente di costruire primitive AAR/AAW, cioè Arbitrary Address Read e Arbitrary Address Write, estremamente potenti.
CMD_GETDATA, infatti, usa copy_to_user per trasferire dati dal kernel allo userland:
1 | if (copy_to_user(req.ptr, ctx->data, req.len)) return -EINVAL; |
Funzioni come copy_to_user e copy_from_user sono progettate per fallire senza crashare anche se ricevono indirizzi non mappati. Questo significa che, anche con KASLR attivo, si può provare a leggere indirizzi arbitrari in modo bruteforce finché una copy_to_user non va a buon fine.
Prima di tutto, però, costruiamo e testiamo le primitive AAR/AAW su memoria userland:
1 | XorCipher *nullptr = NULL; |
Le primitive funzionano:
A questo punto si può scegliere liberamente la strategia finale: cercare il base address del kernel, individuare la struttura cred, e così via. Il codice di exploit di esempio è scaricabile da qui.
cred e il base address del kernel, e misura quale tecnica risulta mediamente più veloce. Quali sono vantaggi e svantaggi di ciascun approccio?
Naturalmente, anche l’handler di
lseekdeve essere implementato correttamente dal modulo. ↩︎