
CVE-2020-25578 и CVE-2020-25579: некоторые ошибки утечки информации в FreeBSD, найденные мной в 2020 году.
ufs_create и не нашел баговstruct dirent, выделенных в стекеmsdosfs_readdir неполное. Они исправили один экземпляр ошибки, но не второй.mqueuefs, autofs, smbfs и tmpfs, которые позволяют извлечь полный 8-байтовый указатель. Написал PoC для подтверждения.Как упоминалось выше, исходная ошибка, которую я нашел, находилась в msdosfs_readdir во время анализа патча для приведенного выше коммита.
Основной поток вызова readdir в FreeBSD выглядит следующим образом:
#include <dirent.h>
int main(void) {
struct dirent *dp;
DIR *dirp;
dirp = opendir("./somedir");
dp = readdir(dirp);
}
В зависимости от файловой системы, в которой находится somedir, может быть вызвана любая из множества функций *_readdir в ядре FreeBSD.
Вышеупомянутый патч добавляет функцию под названием dirent_terminate, которая должна вызываться перед возвратом объекта struct dirent в пользовательское пространство (часто это делается с помощью функции uiomove). Эта функция обнуляет выравнивающие байты, а также оставшиеся байты в поле d_name структуры. Определение struct dirent находится здесь.
Глядя на патч, в строке 1562, можно увидеть, что dirent_terminate вызывается с переменной dirbuf в качестве аргумента. После этого uiomove вызывается для копирования содержимого dirbuf обратно в пользовательское пространство. Однако обратите внимание, что эти строки кода находятся внутри блока этого оператора if. Комментарий над этим оператором if объясняет, что эта ветвь выполняется только при вызове readdir на корне файловой системы MSDOS, поэтому мы можем просто пропустить этот оператор if, вызвав readdir в любой поддиректории за пределами корня файловой системы.
Далее мы видим еще один вызов uiomove в строке 1691. Однако, внимательно читая код, вы увидите, что в этом случае dirent_terminate не вызывается, что означает, что выравнивающие байты останутся неинициализированными. К сожалению, поле d_name было обнулено в начале этой функции (здесь), поэтому мы не можем получить большую утечку.
Сначала у меня не было USB-накопителя, поэтому мне пришлось придумать способ смонтировать файловую систему MSDOS. Следующее работает:
$ 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
PoC можно найти в файле original_poc.c. Просто скомпилируйте с помощью clang и запустите из той же директории, что и команды выше, и вы увидите выведенные утекшие байты.
Я начал искать варианты этой ошибки. Кажется, я просто использовал grep для uiomove\(&.*,, что дало около 15-20 результатов, и я вручную проверил их все. К сожалению, ни один из вариантов не существует в FreeBSD по умолчанию (файловые системы должны быть вручную включены / скомпилированы в ядро). Функции с вариантами следующие:
mqfs_readdirtmpfs_dir_getdotdenttmpfs_dir_getdotdotdentsmbfs_readvdirautofs_readdir_oneОшибка абсолютно одинакова во всех этих функциях, поэтому я рассмотрю только mqfs_readdir.
struct dirent entry выделяется в стекеdirent_terminate для обнуления выравнивающих байтов и полей d_name структурыvfs_read_dirent. Эта функция вызовет uiomove для копирования структуры в пользовательское пространствоПока все выглядит хорошо, не так ли? Не обязательно. Мы должны убедиться, что все поля структуры инициализированы. Если вы внимательно посмотрите на код, вы увидите, что поле d_off остается неинициализированным. Тип этого поля — off_t, что по сути является int64_t. Когда структура копируется в пользовательское пространство, мы получаем неинициализированные данные в этом поле.
Этот же PoC будет работать для всех вариантов, вам просто нужно запустить его на другой файловой системе. Для mqueuefs сделайте следующее (требуется, чтобы mqueuefs была включена / скомпилирована в ядро):
$ mkdir ./temp
$ sudo mount -t mqueuefs null ./temp
Сам PoC можно найти в файле variants_poc.c. Просто скомпилируйте с помощью clang и запустите из той же директории, что и команды выше. Вы увидите выведенные указатели ядра (предположительно, один указатель стека и один указатель секции кода/кучи, я не проверял).