Разбор и обнаружение уязвимости OverlayFS (CVE-2023-0386)

8a449e915918d138f5f8aa53f10f8bd5.jpg

Привет, Хабр!

Я Алексей, исследователь‑аналитик киберугроз в компании R‑Vision. Сегодня я продолжу выявление вредоносных активностей в Linux-системах и рассмотрю новую критичную уязвимость, получившую идентификатор CVE-2023–0386. Уязвимость, обнаруженная в ядре Linux, затронула версии ядра до 6.2 и связана с файловой системой OverlayFS. Она опасна тем, что позволяет непривилегированному пользователю создавать исполняемые файлы с SUID-битом и с их помощью получать привилегии root.

На просторах сети уже можно найти несколько доступных вариаций эксплуатации CVE-2023–0386 (PoC’ов), некоторые из них представлены под спойлером ниже:

Варианты эксплуатации

В этой статье я хочу разобрать с вами принципы работы данной узвимости и рассмотреть, каким образом мы можем обнаружить ее в системе.

Как работает уязвимость

Для понимая работы CVE-2023–0386, давайте посмотрим, как она была исправлена в ядре Linux с помощью недавно вышедшего патча:

diff --git a/fs/overlayfs/copy_up.c b/fs/overlayfs/copy_up.c
index 140f2742074d4..c14e90764e356 100644
--- a/fs/overlayfs/copy_up.c
+++ b/fs/overlayfs/copy_up.c
@@ -1011,6 +1011,10 @@ static int ovl_copy_up_one(struct dentry *parent, struct dentry *dentry,
 	if (err)
 		return err;
 
+	if (!kuid_has_mapping(current_user_ns(), ctx.stat.uid) ||
+	    !kgid_has_mapping(current_user_ns(), ctx.stat.gid))
+		return -EOVERFLOW;
+
 	ctx.metacopy = ovl_need_meta_copy_up(dentry, ctx.stat.mode, flags);
 
 	if (parent) {

В нем мы видим дополнительное условие, добавленное в ovl_copy_up_on — функцию из библиотеки ядра Linux.

Данная функция относится к файловой системе OverlayFS, позволяющей объеднять несколько файловых систем в единую иерархию, и используется для копирования файла или каталога из нижнего слоя (lower layer) в верхний слой (upper layer) OverlayFS. Эта операция дает возможность создать или обновить файлы и каталоги в верхней файловой системе, не затрагивая исходные файлы в нижней.

Но вернемся к условию, оно состоит из двух частей кода:

  1. !kuid_has_mapping(current_user_ns(), ctx.stat.uid) — проверяет, есть ли сопоставление (mapping) для UID, указанного в переменной ctx.stat.uid;

  2. !kgid_has_mapping(current_user_ns(), ctx.stat.gid) — проверяет тоже самое только для GID.

При этом если хотя бы одно из условий возвращает истину (true), то возвращается и значение -EOVERFLOW (оно обычно используется для обозначения переполнения или некорректных значений), и тогда функция не будет выполнена.

Способ эксплуатации уязвимости

А теперь посмотрим какие шаги необходимо предпринять для эксплуатация уязвимости. Для этого нам понадобится:

  1. Cоздать FUSE файловую систему из под непривилегированного пользователя (FUSE, Filesystem in Userspace). FUSE — механизм ядра операционной системы Linux, который позволяет пользователям создавать файловые системы в пространстве пользователя, а не в ядре. Это означает, что файловые системы FUSE не требуют привилегий root для установки и использования;

  2. Внутри этой файловой системы поместить вредоносный файл, который мы хотим выполнить. FUSE позволяет выставить SUID бит и владельца root для данного файла. В одном из PoC для этого используется С-код;

  3. Создать новое пространство имен (namespace) для пользователя (user) и монтирования (mount). Для этого можно воспользоваться командой unshare:

unshare -Urm
  1. В новом пространстве имен мы получаем возможность монтирования, для которого обычно требуются права root.Теперь можно смонтировать OverlayFS, запустив команду ниже. В ней для нижней директории (lowerdir) будет использоваться созданная ранее FUSE с исполняемым файлом, а для верхней (upperdir) любая директория, доступная для записи:

mount -t overlay overlay -o lowerdir=fuse_dir,upperdir=upperdir,workdir=workdir overlay_mount_point
  1. Далее нужно «переместить» наш файл с SUID-битом из нижней директории в верхнюю. Для этого достаточно выполнить команду touch для файла в OverlayFS. Это не изменит сам файл, но изменит метку времени последнего доступа к файлу. Данного изменения будет достаточно для запуска процесса записи OverlayFS в верхнюю директорию. И поскольку в непропатченном ядре нет условия, описанного выше, файл будет записан в верхнюю директорию со всеми правами. Ну, а дальше достаточно выйти из нашего пользовательского пространства и запустить полученный файл из «верхней» директории.

Для более наглядного понимая процесса эксплуатации, представлю описанные мною шаги в виде схемы ниже:

Рисунок 1 - Эксплуатация уязвимости CVE-2023-0386

Рисунок 1 — Эксплуатация уязвимости CVE-2023–0386

Разобравшись с принципом работы уязвимости, давайте выясним как можно мониторить подобную активность.

Обнаружение CVE-2023–0386

Повышение привилегий через исполняемые файлы с SUID-битом до сих пор является одним из наиболее популярных способов у злоумышленников, согласно отчету компании red-canary. В данной уязвимости также применяется эта техника, поэтому детектирование повышения привилегий через SUID-бит в данном случае будет также актуально. Но CVE-2023–0386 позволяет создать любой файл с SUID-битом, поэтому мы будем рассматривать именно обнаружение процесса эксплуатации самой уязвимости.

Для этого нам будет достаточно мониторить системный вызов mount. Так как он вызывается только привилегированным пользователем, то вызов с UID не равным 0 может свидетельствовать о вызове из пространства имен (namespace).

Также mount хранит информацию о типе вызываемой файловой системы. Ниже показано, как данный системный вызов будет выглядеть в ядре:

mount("overlay", "/home/sea-admin/cveOver/overlay_mount_point", "overlay", 0, "lowerdir=hello_mount_point,upper"...) = 0

А для мониторинга в реальном времени посмотрим, как mount представлен в логе auditd. С этой целью добавим правило, которое позволит отследить нужный нам системный вызов:

-a always,exit -F arch=b64 -S mount -F key=mount

И увидим, как mount()выглядит в auditd:

type=SYSCALL msg=audit(1685308769.215:832): arch=c000003e syscall=165 success=yes exit=0 a0=5615b38dbb70 a1=5615b286c7d4 a2=5615b38dbbb0 a3=6 items=5 ppid=73263 pid=73264 auid=1000 uid=1001 gid=1001 euid=0 suid=0 fsuid=0 egid=1001 sgid=1001 fsgid=1001 tty=pts1 ses=1 comm="fusermount" exe="/usr/bin/fusermount3" subj=unconfined key="mount" ARCH=x86_64 SYSCALL=mount AUID="sea-admin" UID="sea-admin" GID="sea-admin" EUID="sea-admin" SUID="sea-admin" FSUID="sea-admin" EGID="sea-admin" SGID="sea-admin" FSGID="sea-admin"

Но как можно заметить, в этом событии мы не обнаружили значений аргументов a0-a2, а только ссылки на их адресное пространство. Так как данные аргументы хранят информацию о типе файловой системы и точках монтирования, они также важны для детектирования.

Поэтому мы попробуем получить их с помощью eBPF, а для этого напишем следующий С-код:

BPF_PERF_OUTPUT(args);

#include 
// структура данных для передачи в пользовательское пространство
struct args {
    char task[TASK_COMM_LEN];
    u32 m_uid;
    char m_source[32];
    char m_target[32];
    char m_filesystem[32];
    char m_data[64];
    long m_flags;
};
// основная функция для перехвата системных вызовов mount()
int syscall__mount(struct pt_regs *ctx, const char *source, const char *target, const char *filesystemtype, unsigned long mountflags, const void *data){

       struct args arg_data = {};
       //вызывающая команда
       bpf_get_current_comm(&arg_data.task, sizeof(&arg_data.task));
       //uid пользователя
       arg_data.m_uid = bpf_get_current_uid_gid();
       //аргументы системного вызова
       bpf_probe_read(&arg_data.m_source, sizeof(arg_data.m_source), &*source);
       bpf_probe_read(&arg_data.m_target, sizeof(arg_data.m_target), &*target);
       bpf_probe_read(&arg_data.m_filesystem, sizeof(arg_data.m_filesystem), &*filesystemtype);
       bpf_probe_read(&arg_data.m_flags, sizeof(arg_data.m_flags), &mountflags);
       bpf_probe_read(&arg_data.m_data, sizeof(arg_data.m_data), &*data);
       
       args.perf_submit(ctx, &arg_data, sizeof(arg_data));

       return 0;
}

Данный код мониторит все системные вызовы mount() и передает их аргументы в пользовательское пространство через perf ring buffer. Для загрузки в пользовательское пространство данного BPF и вывода результата воспользуемся Python «оберткой», пример которой представлен ниже под спойлером:

Пример обертки

from bcc import BPF

# Читаем ebpf код из файла
with open("bpf.c") as file:
    bpf_txt = file.read()
    
def handle_event(cpu, data, size):
    output = bpf_ctx["args"].event(data)
    print("{:15} {:<5} {:<10} {:<15} {:<10} {:<10} {:<30}".format(output.task.decode("utf-8") , output.m_uid,
        output.m_source.decode("utf-8"), output.m_target.decode("utf-8"), output.m_filesystem.decode("utf-8"), output.m_flags, output.m_data.decode("utf-8")))

bpf_ctx = BPF(text=bpf_txt)
syscall_fnname = bpf_ctx.get_syscall_fnname("mount")
# Присоединяем наш ebpf к системному вызову bpf
bpf_ctx.attach_kprobe(event=syscall_fnname, fn_name="syscall__mount")
bpf_ctx["args"].open_perf_buffer(handle_event)

# Настраиваем вывод в консоль
print("{:<15} {:<5} {:<10} {:<15} {:<10} {:<10} {:<30}".format('exe_task',"uid",
    'source', 'target', 'fs type','flags', 'data'))
while 1:
    try:
        bpf_ctx.perf_buffer_poll()
    except KeyboardInterrupt:
        print()
        exit()

После загрузки нашего BPF и эксплуатации уязвимости, нам станет доступен следующий вывод:

exe_task        uid   source     target          fs type    flags      data
fuse            1000  /dev/fuse  ./ovlcap/lower  fuse       6          fd=3,rootmode=40000,user_id=1000,group_id=1000
fusermo         1000  /dev/fuse  .               fuse       6          fd=4,rootmode=40000,user_id=1000,group_id=1000
exp             1000  overlay    ./ovlcap/merge  overlay    0          lowerdir=./ovlcap/lower,upperdir=./ovlcap/upper,workdir=./ovlcap

И здесь мы увидим, что изначально была создана файловая система fuse из-под пользователя с UID 1000 с точкой монтирования в ./ovlcap/lower.

В то же время в конце вывода показано создание файловой системы Overlay из-под того же пользователя в «нижний уровень», которой передается та же директория ./ovlcap/lower. Логическое правило будет выглядеть так:

event1.uid = event2.uid != 0 &
event1.fs_type = fuse &
event2.fs_type = overlay &
event1.target = event2.data.lowerdir

Данная логика полностью соответствует принципу работы уязвимости и маловероятно встретить ее в легитимной работе приложений. Реализовать подобное правило можно в SIEM-системе, передав ей данные логи, или непосредственно в самом модуле eBPF.

Вывод

Уязвимости ядра Linux могут создавать большие возможности для атакующего, как например уязвимость OverlayFS (CVE-2023–0386), которая сегодня была рассмотрена этой в статье. Как мы могли заметить, стандартных механизмов auditd явно недостаточно, чтобы обеспечить корректное детектирование эксплуатации данной уязвимости. Поэтому мною было предложено решение, основанное на механизме eBPF. Оно позволило увидеть все аргументы системного вызова mount() и сформировать корреляционное правило.

Чтобы защититься от подобных уязвимостей, помимо обнаружения их эксплуатации, я также рекомендую вам регулярно обновлять свои информационные системы.

Спасибо за внимание! Если у вас остались вопросы — пишите в комментариях :)

Автор: Епишев Алексей (@alexepishev), аналитик-исследователь киберугроз в компании R-Vision

© Habrahabr.ru