Molti pensano: “Ho studiato più o meno tutto il pwn in userland, ma il kernel sembra troppo difficile per iniziare”. In realtà, in alcuni casi gli attacchi al kernel sono sorprendentemente semplici.
In questa sezione spiegheremo le differenze fra exploit userland e kernel exploit, oltre agli elementi base dell’ambiente di lavoro.

Caratteristiche del kernel exploit

Partiamo dalle differenze fondamentali fra vulnerabilità in userland e vulnerabilità in kernel space.

Bersaglio dell’attacco

La differenza più importante fra exploit userland e kernel exploit riguarda lo scopo finale.
Negli exploit userland visti finora, spesso l’obiettivo era ottenere esecuzione arbitraria di comandi. Nei kernel exploit, invece, il risultato tipico è l’elevazione di privilegi. Si assume che l’attaccante abbia già un qualche accesso alla macchina vittima e che usi il kernel exploit per arrivare a root[1]. Questa classe di attacco viene chiamata LPE (Local Privilege Escalation).
Naturalmente esistono anche vulnerabilità userland che permettono escalation di privilegi, ma in quel caso succede perché il programma vulnerabile gira già con privilegi elevati. Nel kernel exploit, i bersagli principali sono due:

  1. il kernel Linux
  2. i moduli del kernel

Il codice interno al kernel Linux, come system call e filesystem, gira con privilegi root, quindi un bug nel kernel può portare direttamente a LPE.
L’altro bersaglio tipico sono i moduli del kernel, per esempio i device driver. I driver offrono allo userland un’interfaccia verso risorse di basso livello, soprattutto dispositivi esterni come stampanti e simili. Anche loro girano con privilegi root[2], quindi una vulnerabilità al loro interno può portare a LPE.

Modalità di attacco

Negli exploit userland, di solito si attacca un servizio inviandogli input. Per questo la maggior parte degli exploit viene scritta in Python o linguaggi simili.
Nel kernel exploit, invece, il bersaglio è il sistema operativo o un driver, quindi si lavora a un livello molto più basso. Per questo motivo gli exploit vengono tipicamente scritti in C. Naturalmente si potrebbe usare anche Python, ma nella pratica, soprattutto in ambienti CTF o VM minimali, Python spesso non è nemmeno disponibile sulla macchina target.

Anche in questo sito gli exploit saranno scritti in C. Più avanti useremo un compilatore chiamato musl-gcc.

Risorse condivise

Un’altra caratteristica importante del kernel exploit è che le risorse sono condivise.
In userland, in genere, esiste un processo bersaglio specifico e l’attacco avviene contro quello. Invece il kernel Linux e i driver sono condivisi da tutti i processi che usano il sistema. Chiunque può invocare system call in qualsiasi momento, e non si sa quando o da chi verrà usato un certo driver. Questo significa che, quando si scrive codice in kernel space, bisogna ragionare sempre come se si fosse in un ambiente multi-thread: altrimenti è molto facile introdurre vulnerabilità.

Lupetto

Quindi, quando usi dati condivisi come le variabili globali, devi proteggerli con dei lock.
Programmare in kernel space è bello impegnativo.

Heap condiviso

Inoltre, l’heap del kernel è condiviso da tutti i driver e dal kernel stesso.
Negli exploit userland, ogni programma ha in pratica il proprio heap, quindi anche in presenza di uno Heap Overflow lo sfruttamento dipende fortemente da come è scritto quel programma. Nel kernel, invece, basta un singolo heap overflow in un driver per corrompere anche dati allocati nelle vicinanze dal kernel o da altri driver.

Dal punto di vista dell’attaccante, questo ha pro e contro. Il vantaggio è che anche vulnerabilità apparentemente piccole sull’heap possono portare con alta probabilità a una LPE. Nel kernel esistono molti oggetti che contengono function pointer, quindi spesso basta corromperne uno per ottenere facilmente controllo di RIP. Lo svantaggio è che lo stato dell’heap è molto meno prevedibile, perché dipende da tutto il sistema. In userland, per programmi semplici, lo stato dell’heap era spesso deterministico rispetto all’input, e questo rendeva possibili exploit dell’heap molto sofisticati. Nel kernel, invece, non si sa quali dati ci saranno dopo il chunk overflowato, o chi riutilizzerà un certo indirizzo dopo una free.

Lupetto

Quindi nei kernel exploit l'Heap Spray è particolarmente importante.

Le vulnerabilità, in sé, non sono molto diverse da quelle userland. Stack Overflow e Use-after-Free esistono anche in kernel space. Anche sui frame dello stack di un driver si può applicare Stack Canary. Esistono però anche vulnerabilità tipiche del kernel, e le vedremo più avanti.

Uso di qemu

Quando si sviluppa un kernel exploit per Linux, si esegue quasi sempre il kernel all’interno di un emulatore o di una VM per poterlo debuggare. In teoria va bene qualunque macchina virtuale, ma qemu è lo standard di fatto e quindi useremo quello.

Installa qemu-system nell’ambiente che preferisci. Per esempio:

1
# apt install qemu-system

Immagine disco

Quando qemu avvia una macchina, oltre al kernel serve anche un’immagine disco che verrà montata come root filesystem.
Le immagini disco vengono in genere distribuite come filesystem grezzi (per esempio ext) oppure in formato cpio.
Se si tratta di un filesystem vero e proprio, basta montarlo con mount per modificarne il contenuto.

1
2
# mkdir root
# mount rootfs.img root

Negli esercizi di questo sito useremo cpio, che è il formato più comune nei CTF ed è anche leggero.
Per estrarre i file si può usare:

1
2
# mkdir root
# cd root; cpio -idv < ../rootfs.cpio

Dopo aver aggiunto o modificato file, si può ricreare l’archivio così:

1
# find . -print0 | cpio -o --format=newc --null > ../rootfs_updated.cpio

Talvolta il cpio è anche compresso con gz; in quel caso bisogna decomprimerlo e ricomprimerlo di conseguenza.

Ricorda che cpio conserva anche i permessi e i proprietari dei file. Quando modifichi l’immagine, devi quindi fare attenzione ad assegnare correttamente root come owner. Negli esempi sopra i comandi vengono eseguiti come root, quindi va bene così. Se vuoi evitare di lavorare come root, puoi usare l’opzione --owner=root durante il repack.

1
2
3
4
$ mkdir root
$ cd root; cpio -idv < ../rootfs.cpio
...
$ find . -print0 | cpio -o --format=newc --null --owner=root > ../rootfs_updated.cpio

Scarica i file di LK01 e svolgi le seguenti operazioni.
(1) Esegui run.sh e verifica che Linux si avvii correttamente.
(2) Modifica rootfs.cpio in modo che all'avvio si ottenga una shell con privilegi root. (Suggerimento: cerca lo script che stampa i messaggi di boot.)

  1. Esistono anche attacchi molto più avanzati, come SMBGhost, in cui un problema nello stack di protocollo permette un vero kernel exploit da remoto. ↩︎

  2. I filesystem e i character device sono di solito implementati come moduli del kernel, ma funzionalità come FUSE e CUSE hanno reso possibile implementarli anche in userland. ↩︎