
CVE-2020-25578 e CVE-2020-25579: Alcuni bug di leak di informazioni in FreeBSD che ho trovato nel 2020.
ufs_create e trovato 0 bugstruct dirent allocati sullo stackmsdosfs_readdir è incompleta. Hanno corretto un'istanza del bug, ma non una seconda.mqueuefs, autofs, smbfs e tmpfs che mi permettono di far fuoriuscire un puntatore completo di 8 byte. Ho scritto un PoC per confermare.Come accennato in precedenza, il bug originale che ho trovato era in msdosfs_readdir mentre analizzavo la patch per il commit linkato sopra.
Il flusso di base per chiamare readdir su FreeBSD è il seguente:
#include <dirent.h>
int main(void) {
struct dirent *dp;
DIR *dirp;
dirp = opendir("./somedir");
dp = readdir(dirp);
}
A seconda del filesystem in cui risiede somedir, potrebbe essere chiamata una qualsiasi delle numerose funzioni *_readdir nel kernel di FreeBSD.
La patch sopra aggiunge una funzione chiamata dirent_terminate che ha lo scopo di essere chiamata prima che un oggetto struct dirent venga restituito allo userspace (spesso tramite la funzione uiomove). Questa funzione azzera i byte di padding e tutti i byte rimanenti nel campo d_name della struct. La definizione di struct dirent è qui.
Guardando la patch, alla riga 1562, si può vedere dirent_terminate chiamata con la variabile dirbuf come argomento. Subito dopo, uiomove viene chiamata per copiare il contenuto di dirbuf di nuovo nello userspace. Tuttavia, nota che queste righe di codice sono all'interno del blocco di questo if. Il commento sopra questo if spiega che questo ramo viene seguito solo se readdir viene chiamato sulla radice del filesystem MSDOS, quindi possiamo semplicemente saltare questo if chiamando readdir in qualsiasi sottodirectory oltre la radice del filesystem.
Più avanti, vediamo un'altra chiamata a uiomove alla riga 1691. Tuttavia, leggendo attentamente il codice, vedrai che dirent_terminate non viene chiamata in questa occasione, il che significa che i byte di padding rimarranno non inizializzati. Purtroppo il campo d_name è stato azzerato all'inizio di questa funzione (qui), quindi non possiamo ottenere una fuga più grande.
Per prima cosa, non avevo una chiavetta USB, quindi ho dovuto trovare un modo per montare un filesystem MSDOS. Quanto segue funziona:
$ dd if=/dev/zero of=test.img bs=512 count=256000
$ sudo mdconfig -a -t vnode -f test.img
$ sudo newfs_msdos -s 131072000 /dev/md1 # My mdconfig returned md1
$ mkdir ./temp
$ sudo mount -t msdosfs /dev/md1 ./temp
$ mkdir ./temp/test_dir
Il PoC si trova in original_poc.c. Basta compilare con clang ed eseguirlo dalla stessa directory dei comandi sopra, e vedrai i byte fuoriusciti stampati.
Ho iniziato a cercare varianti di questo bug. Penso di aver semplicemente usato grep per uiomove\(&.*, che ha restituito circa 15-20 risultati, e li ho controllati tutti a mano. Purtroppo nessuna delle varianti è presente in FreeBSD di default (i filesystem devono essere abilitati / compilati manualmente nel kernel). Le funzioni con le varianti sono le seguenti:
mqfs_readdirtmpfs_dir_getdotdenttmpfs_dir_getdotdotdentsmbfs_readvdirautofs_readdir_oneIl bug è esattamente lo stesso in tutte queste funzioni, quindi tratterò solo mqfs_readdir.
struct dirent entry sullo stackdirent_terminate viene chiamata per azzerare i campi padding + d_name della structvfs_read_dirent viene chiamata. Questa funzione chiamerà uiomove per copiare la struct nello userspaceTutto sembra a posto fin qui, vero? Non necessariamente. Dobbiamo assicurarci che tutti i campi della struct siano inizializzati. Se guardi attentamente il codice, vedrai che il campo d_off viene lasciato non inizializzato. Il tipo di questo campo è off_t, che è essenzialmente un int64_t. Quando la struct viene copiata nello userspace, otteniamo dati non inizializzati in questo campo.
Questo stesso PoC funzionerà per tutte le varianti, devi solo eseguirlo su un filesystem diverso. Per mqueuefs, fai quanto segue (richiede mqueuefs abilitato / compilato nel kernel prima):
$ mkdir ./temp
$ sudo mount -t mqueuefs null ./temp
Il PoC stesso si trova in variants_poc.c. Basta compilare con clang ed eseguirlo dalla stessa directory dei comandi sopra. Vedrai dei puntatori del kernel stampati (presumibilmente un puntatore allo stack e un puntatore a sezione di codice / heap, non ho controllato).