Привет, Хабр! Меня зовут Алексей, в компании R-Vision я занимаюсь исследованием и анализом киберугроз. И сегодня я бы хотел поговорить с вами о Dirty Pipe (CVE-2022-084)  – одной из критичных уязвимостей Linux ядра, которая позволяет злоумышленникам локально повысить привилегии.Такие уязвимости выходят не так часто, но при этом несут особую опасность, так как затрагивают сразу множество дистрибутивов, работающих на уязвимой версии ядра.      Уязвимость Dirty Pipe была публично раскрыта в марте 2022, затронув несколько версий Linux, начиная с версии 5.8. После чего она была исправлена в версиях 5.16.11, 5.15.25, а также 5.10.102. Данные версии ядра популярны во многих известных дистрибутивах, включая тот же Android. И несмотря на то, что с момента выхода патчей прошло достаточно времени, они до сих пор установлены далеко не во всех компаниях.  Особенность Dirty Pipe заключается в том, что она позволяет непривилегированному (non-root) пользователю вносить изменения в файл, доступный только для чтения, то есть с правами read для текущего пользователя. По сути это позволяет получить права root`а разными способами. Зачастую уязвимости, подобные Dirty Pipe могут быть проэксплуатированы даже из "ограниченных" сред контейнеров, если на хостах не настроены дополнительные политики безопасности (hardening). Для их устранения обычно требуется обновить само ядро, что в свою очередь может привести к большим рискам для промышленных систем.   В этой статье мною будут рассмотрены способы анализа Dirty Pipe и потенциальные варианты детектирования уязвимости. При построении возможного детекта мы не будем заострять внимание на пост-эксплуатационных шагах, а разберем способ мониторинга обращения к ядру, чтобы распознать попытки эксплуатации узявимости.  Но прежде давайте детальнее ознакомимся с тем, как работает Dirty Pipe.  Для понимания работы Dirty Pipe нам нужно изучить основные системные вызовы или по-другому system calls, которые будут использоваться в процессе эксплуатации уязвимости. Системные вызовы предназначены для общения пользовательского пространства (user space) с простанством ядра (kernel space). В свою очередь, эксплуатация Dirty Pipe стала возможна после добавления флага PIPE_BUF_FLAG_CAN_MERGE в версию ядра 5.8, позволяющего ядру объединить несколько маленьких операций в одну, что улучшает производительность.  Анализ исходного кода от автора уязвимости покажет нам, что основными системными вызовами при ее эксплуатации являются pipe и splice: pipe создает "канал" между двумя процессами, позволяя одному писать в этот канал, а другому читать из него; splice используется для передачи данных между двумя файловыми дескрипторами внутри пространства ядра, но без обращения к пользовательскому пространству. Он позволяет эффективно передавать данные, в том числе и между "каналами" pipe.  Кроме основных вызовов, используются и вполне "стандартные", такие как open, read и write: open дает возможность открывать файл. Заметим, что у него есть аналог openat  для которого указывается относительный путь файла; write - используется  для записи данных; read - применяется для чтения. Этапы эксплуатации Dirty Pipe выглядят следующим образом: Открывается файл с правами на чтение; Специальным образом готовится pipe c предварительной установкой флага PIPE_BUF_FLAG_CAN_MERGE; В подготовленный pipe передаются данные с помощью вызова splice;  Вредоносные данные записываются в тот же pipe; Исходный файл также будет перезаписан, так как установлен флаг PIPE_BUF_FLAG_CAN_MERGE. Для наглядности алгоритма покажу этапы в виде схемы: Теперь давайте еще детальнее изучим работу Dirty Pipe на примере кода эксплоита. Проанализировав множество различных вариаций эксплуатации уязвимости,присутствующих в сети, можно сделать вывод, что принцип работы относительно ядра у них одинаковый. Поэтому для подготовки возможного способа детектирования мы будем использовать все тот же исходный код PoC'a автора уязвимости.   Для удобства ссылки на остальные эксплоиты я добавил ниже под спойлер.  https://github.com/Al1ex/CVE-2022-0847 https://github.com/Arinerron/CVE-2022-0847-DirtyPipe-Exploit https://github.com/antx-code/CVE-2022-0847 https://github.com/bbaranoff/CVE-2022-0847 https://github.com/dadhee/CVE-2022-0847_DirtyPipeExploit https://github.com/imfiver/CVE-2022-0847 https://github.com/lucksec/CVE-2022-0847 https://github.com/AlexisAhmed/CVE-2022-0847-DirtyPipe-Exploits https://github.com/LudovicPatho/CVE-2022-0847 https://github.com/gyaansastra/CVE-2022-0847 https://github.com/crusoe112/DirtyPipePython https://github.com/tmoneypenny/CVE-2022-0847 https://github.com/phuonguno98/CVE-2022-0847-DirtyPipe-Exploits https://github.com/VinuKalana/DirtyPipe-CVE-2022-0847 https://github.com/greenhandatsjtu/CVE-2022-0847 https://github.com/flux10n/CVE-2022-0847-DirtyPipe-Exploits https://github.com/notl0cal/dpipe https://github.com/EagleTube/CVE-2022-0847 В коде мы увидим функцию prepare_pipe(), которая "подготавливает" pipe: сначала она наполняет его данными, а впоследствии опустошает полностью вычитывая все данные с помощью цикла. Это и позволяет создать pipe с флагом PIPE_BUF_FLAG_CAN_MERGE.  На следующем шаге уже используется системный вызов splice, который передает данные с открытого файла в подготовленный pipe: Дальше происходит запись входных данных в дескриптор того же pipe с помощью вызова write: По итогу мы увидим, что данные, поданные на вход, записаны в файле несмотря на то, что он был открыт только на чтение.Разобравшись с функциями в коде, давайте изучим, как они выглядят со стороны ядра Linux и на практике рассмотрим механизм работы Dirty Pipe. Для этого нам снова нужно обратиться к системным вызовам, которые будут использоваться во время эксплуатации уязвимости. Поэтому воспользуемся командой strace, компилируя и запустив PoC c параметрами атакуемого файла и номера бита для записи самого сообщения c помощью команды:  После запуска strace, мы получаем следующий вывод: В выводе видны уже знакомые нам системные вызовы с их аргументами. "Но где же флаг PIPE_BUF_FLAG_CAN_MERGE?" - спросите Вы. Мы не увидим его в обращении к ядру, так как флаг устанавливается внутри ядра. Но чуть позже мы рассмотрим, как же его можно обнаружить. Проанализировав работу Dirty Pipe, перейдем к важной части – возможным вариантам детектирования уязвимости. Использовать для этого я буду модули службу auditd и eBPF,  позволяющие запускать изолированные программы внутри Linux и проводить аудит событий.  Давайте посмотрим как выглядят системные вызовы в самом распространённом демоне аудита Linux - audit. Для этого  запустим мониторинг open, write, pipe (в современных Linux системах это pipe2) и splice. Чтобы осуществить запуск, нам нужно написать следующие правила в auditd: always,exit - мониторинг всех событий после завершения; arch=b64 - архитектуры системы; В параметре -S выставляем название системного вызова для обнаружения; Указываем key для дальнейшего удобства поиска, 1,2,3 для разделения системных вызовов. Настроить правила можно с помощью утилиты auditctl или добавив строки в файл правил, по умолчанию это:/etc/audit/rules.d/audit.rules.В таком случае правила применятся и останутся после перезагрузки демона.  Результат можно посмотреть в лог файле auditd (audit.log) или с помощью команды ausearch, предварительно отфильтровав по названию исполняемого файла, так как данные системные вызовы часто используются в легитимных целях.  Из множества событий, нас будут интересовать следующие: Эти системные вызовы сами по себе легитимны. Однако нам важна их последовательность и наличие одинакового файлового дескриптора a2=5 и a0=5 в вызовах splice и write, равного дескриптору для записи pipe - fd1=5. Такая совокупность событий редко встречается в обычных приложениях и соответствует принципу работы уязвимости. Ниже приведен пример подобного псевдо-правила: Однако события выше по‑прежнему не показывают наличия флага PIPE_BUF_FLAG_CAN_MERGE. Как следствие, отсюда могут возникнуть ложные сработки, поскольку эти вызовы отражают легитимную работу системы. Это является одним из недостатков детекта с помощью auditd. Еще один недостаток данного способа заключается в огромном количестве системных вызовов write , что может привести к сильной нагрузке как на саму операционную систему, так и на SIEM систему. Можно, конечно, сосредоточиться исключительно на системных вызовах splice, но в его аргументах содержится только информация о файловых дескрипторах и длине фрейма. Таким образом, отличить нелегитимный вызов splice только с помощью auditd невозможно.  Если вы увидите подобные варианты детектирования с помощью одного правила auditd, которое приведено ниже или приводится в некоторых источниках (1,2,3) не спешите вносить его в audit.rules.  Основной минус заключается в том, что если добавить дополнительный open любого файла в начало эксплоита, то вы получите a0=0x4 a2=0x6, так как значения файловых дескрипторов вырастут на 1. И так можно делать много раз, что позволяет очень легко обходить подобные правила.  Рассмотрев вариант детекта с помощью auditd,стало очевидным, что он имеет ряд недостатков. Поэтому мы проанализируем еще один способ - через eBPF. Как я уже говорил, уязвимость Dirty Pipe эксплуатируется начиная с версии ядра 5.8. В данной версии присутствует инструмент eBPF, который также может быть задействован для выявления эксплуатации. eBPF (extended Berkeley Packet Filter) – это технология, позволяющая программно обрабатывать сетевые пакеты и события в операционной системе Linux без необходимости напрямую изменять код ядра. "Под капотом" у этой технологии находится виртуальная машина, выполняющая программируемые сценарии, написанные на языке C. Сценарии могут быть загружены в ядро во время выполнения и использоваться для настройки и оптимизации поведения различных компонентов сети и системы. Программы eBPF возможно присоединить к различным системным событиям и вызовам в ядре. Для наглядности мы воспользуемся библиотекой BCC для Python и присоединим наш eBPF модуль к системному вызову splice, так как он встречается реже, чем write и pipe. Это минимизирует затрату ресурсов при обнаружении, также он содержит необходимые файловые дескрипторы. Затем проверим последний Page Buffer Ring (PBR) на наличие флага PIPE_BUF_FLAG_CAN_MERGE. PBR позволяет ядру отслеживать информацию о физическом расположении страниц в памяти, используемых для буферизации ввода-вывода (I/O) данных, таких как: файлы, сетевые пакеты и другие объекты. А теперь давайте посмотрим как eBPF модуль поможет в выявлении эксплуатации DirtyPipe. Для этого воспроизведём следующие действия:  Заберем аргумент файлового дескриптора pipe из системного вызова splice и проверить флаги в PBR; Для чтения информации из ядра воспользуемся BPF Helper bpf_probe_read;  Если флаг PIPE_BUF_FLAG_CAN_MERGE будет найден, получим PID и команду запуска вредоносного процесса с помощью bpf_get_current_pid_tgid() и bpf_get_current_comm(&comm, sizeof(comm); После чего выведем информацию в наше пользовательское пространство, используя функцию bpf_trace_printk. Также с помощью eBPF мы можем проверить следующие аспекты: Открыт ли исходный файл в режиме чтения (read); Является ли файловый дескриптор вывода дескриптором pipe; Пишутся ли в этот же дескриптор другие данные. Но это все возможно увидеть и в auditd. Однако при детекте с помощью eBPF можно будет проводить корреляцию прямо внутри ядра, что позволит значительно сэкономить ресурсы, но это уже будет темой отдельной статьи. Чтобы загрузить eBPF, нам потребуется вспомогательный скрипт, который читает файл с данным модулем и выводит уведомление в консоль о возможных сработках: В результате сработки мы увидим уведомление о запущенной команде, пытающейся эксплуатировать Dirty Pipe: Следственно, можно сделать вывод, что выявление Dirty Pipe с использованием eBPF оказалось эффективнее. Данный вывод представлен больше для демонстрационных целей, поэтому для отправки данных в SIEM лучше настроить нормальный вывод в лог-файл.  Уязвимости ядра могут быть очень опасны и порой их сложно детектировать из-за ограниченности популярных средств логирования в Linux. Рассмотрев два основных варианта детектирования, можно сделать вывод, что  auditd предоставляет множество вариантов для аудита Linux, но зачастую их бывает недостаточно для обнаружения некоторых аспектов ядра. В данном примере мы увидели, что с помощью него нельзя обнаружить флаги в буфере pipe . В таком случае приходится прибегать к таким инструментам, как модуль eBPF.  В свою очередь, данный модуль позволяет самостоятельно обращаться к нужным элементам ядра и получать о них информацию. С помощью eBPF, проводя мониторинг системного вызова splice - неотъемлемой части эксплуатации уязвимости Dirty Pipe,  мы смогли обнаружить флаг  PIPE_BUF_FLAG_CAN_MERGE, чего не удавалось сделать с помощью auditd. В данном случае, этот способ детекта оказался эффективнее.  Если у вас остались вопросы, пишите в комментариях! Буду рад, если статья будет полезной для вас!  Автор: Епишев Алексей, аналитик-исследователь киберугроз в компании R-Vision  Профиль команды SEA