
CVE-2020-25578 y CVE-2020-25579: Algunos bugs de fuga de información de FreeBSD que encontré en 2020.
ufs_create y encuentro 0 bugsstruct dirent asignados en la pilamsdosfs_readdir está incompleto. Parchearon una instancia del bug, pero no una segunda.mqueuefs, autofs, smbfs y tmpfs que me permiten filtrar un puntero completo de 8 bytes. Escribo PoC para confirmar.Como se mencionó anteriormente, el bug original que encontré estaba en msdosfs_readdir mientras analizaba el parche del commit enlazado arriba.
El flujo básico para llamar a readdir en FreeBSD es el siguiente:
#include <dirent.h>
int main(void) {
struct dirent *dp;
DIR *dirp;
dirp = opendir("./somedir");
dp = readdir(dirp);
}
Dependiendo del sistema de archivos en el que resida somedir, se puede llamar a cualquiera de las muchas funciones *_readdir en el kernel de FreeBSD.
El parche anterior agrega una función llamada dirent_terminate que está destinada a ser llamada antes de que un objeto struct dirent sea devuelto al espacio de usuario (a menudo usando la función uiomove). Esta función pondrá a cero los bytes de relleno así como los bytes restantes en el campo d_name de la estructura. La definición de struct dirent está aquí.
Mirando el parche, en la línea 1562, se puede ver que se llama a dirent_terminate con la variable dirbuf como argumento. A continuación, uiomove es llamado para copiar el contenido de dirbuf de vuelta al espacio de usuario. Sin embargo, nota que estas líneas de código están dentro del bloque de esta sentencia if. El comentario sobre esta sentencia if explica que esta rama solo se toma si readdir es llamado en la raíz del sistema de archivos MSDOS, por lo que podemos simplemente saltar esta sentencia if llamando a readdir en cualquier subdirectorio más allá de la raíz del sistema de archivos.
Más abajo, vemos otra llamada a uiomove en la línea 1691. Sin embargo, leyendo el código con atención, verás que dirent_terminate no es llamado en esta instancia, lo que significa que los bytes de relleno permanecerán sin inicializar. Desafortunadamente, el campo d_name fue puesto a cero al inicio de esta función (aquí), por lo que no podemos obtener una fuga más grande.
Primero, no tenía una unidad USB, así que tuve que encontrar una forma de montar un sistema de archivos MSDOS. Lo siguiente funciona:
$ 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 # Mi mdconfig devolvió md1
$ mkdir ./temp
$ sudo mount -t msdosfs /dev/md1 ./temp
$ mkdir ./temp/test_dir
El PoC se puede encontrar en original_poc.c. Simplemente compílalo con clang y ejecútalo desde el mismo directorio que los comandos anteriores, y verás los bytes filtrados impresos.
Empecé a buscar variantes de esto. Creo que simplemente hice un grep de uiomove\(&.*, que devolvió alrededor de 15-20 resultados, y los revisé todos manualmente. Desafortunadamente, ninguna de las variantes existe en FreeBSD por defecto (los sistemas de archivos tienen que ser habilitados manualmente / compilados en el kernel). Las funciones con las variantes son las siguientes:
mqfs_readdirtmpfs_dir_getdotdenttmpfs_dir_getdotdotdentsmbfs_readvdirautofs_readdir_oneEl bug es exactamente el mismo en todas estas funciones, así que solo cubriré mqfs_readdir.
struct dirent entry en la piladirent_terminate para poner a cero los campos de relleno + d_name de la estructuravfs_read_dirent. Esta función llamará a uiomove para copiar la estructura al espacio de usuarioTodo se ve bien hasta ahora, ¿verdad? No necesariamente. Tenemos que asegurarnos de que todos los campos de la estructura estén inicializados. Si observas el código con atención, verás que el campo d_off queda sin inicializar. El tipo de este campo es off_t, que es esencialmente un int64_t. Cuando la estructura se copia al espacio de usuario, obtenemos datos no inicializados en este campo.
Este mismo PoC funcionará para todas las variantes, solo tienes que ejecutarlo en un sistema de archivos diferente. Para mqueuefs, haz lo siguiente (primero necesita mqueuefs habilitado / compilado en el kernel):
$ mkdir ./temp
$ sudo mount -t mqueuefs null ./temp
El PoC en sí se puede encontrar en variants_poc.c. Simplemente compílalo con clang y ejecútalo desde el mismo directorio que los comandos anteriores. Verás punteros del kernel impresos (presumiblemente un puntero de pila y un puntero de sección de código / heap, no lo verifiqué).