+7 (812) 677-17-05

CVE 2022-0847: Исследование уязвимости Dirty Pipe

#

Предисловие:

Данная статья является переводом англоязычного исследования, посвященного разбору уязвимости Dirty Pipe и непосредственно эксплоита, позволяющего ею воспользоваться для локального повышения привилегий.

Оригинал

Введение:

Уязвимость Dirty Pipe была обнаружена в ядре Linux исследователем Максом Келлерманном(Max Kellermann) и описана им здесь. Несмотря на то, что статья Келлерманна – отличный ресурс, содержащий всю необходимую информацию для понимания ошибки ядра, все таки она предполагает некоторое знакомство с ядром Linux.

Коротко о Pipe, Page и файловых дескрипторах в Linux

Далее в статье еще будут более подробно разобраны такие понятия, как pipe, page и file descriptor. Но для начала давайте вспомним общую концепцию работы этих элементов.

Pipe (канал, конвейер) — это однонаправленный метод взаимодействия процессов
между собой.

Канал позволяет процессу получать входные данные от предыдущего, используя pipe buffer(буфер канала или буфер конвейера).

Самый простой пример, который наглядно показывает работу Pipe-ов приведен на скриншоте ниже:

В данном примере выходные данные команды cat используются в качестве входных данных для команды grep, применяя Pipe.

Page(страница) представляет собой блок данных, чаще всего размером 4096 байт. Ядро Linux разбивает данные на страницы и работает непосредственно с ними, а не с целыми файлами сразу.

В механизме канала(pipe) есть флаг “PIPE_BUF_FLAG_CAN_MERGE”, который указывает, разрешено ли слияние большего количества данных в буфер канала. Если размер скопированной страницы меньше 4096 байт, то в буфер канала можно добавить больше данных.

File descriptor (файловый дескриптор) — это число, которое однозначно идентифицирует открытый файл в ОС.

Когда программа запрашивает открытие файла, ядро в свою очередь:

  1. Предоставляет доступ.
  2. Создает запись в глобальной таблице файлов.
  3. Предоставляет программе расположение этой записи.

Когда процесс делает успешный запрос на открытие файла, ядро возвращает файловый дескриптор, который указывает на запись в глобальной файловой таблице ядра.

Запись таблицы файлов содержит информацию, такую как:

  1. Индекс файла
  2. Смещение в байтах
  3. Ограничение доступа для данного потока данных (read-only, write-only и т.д.).

После обсуждения основных понятий, давайте перейдем к уязвимости.

Разбор уязвимости и PoC

На данный момент мы уже можем предположить, что из себя представляет уязвимость Dirty Pipe: она позволяет перезаписать кешированные данные любого файла, который нам разрешено открывать(достаточно будет прав на чтение), фактически не помечая страницы с перезаписанной страницей кеша как “грязные”(всякий раз, когда процесс изменяет какие либо данные, на соответствующую страницу устанавливается флаг “PG_dirty”, тем самым она помечается как “грязная”).

Таким образом мы можем воспользоваться данной уязвимостью, чтобы повысить привилегии в локальной системе путем перезаписи файла. В нашем случае, добавив пользователя с неограниченными правами в /etc/passwd.

Screenshots

Но перейдем непосредственно к эксплоиту.

Первым делом наш PoC открывает файл для чтения, без каких-либо дополнительных
флагов.

int tfd;
...
pause_for_inspection("About to open() file");
tfd = open("./target_file", O_RDONLY);

PoC source

Функция ядра, обрабатывающая наш open() вызов пользовательского пространства,
называется do_sys_openat2(). Она пытается получить файл в желаемом ей режиме, и, если это получается, устанавливает новый файловый дескриптор, который поддерживается файлом, и возвращает его в виде целого числа.

Код

source

Выполнение вызова do_filp_open() связано с риском потеряться в “джунглях” (виртуальной) файловой системы, поэтому мы поместим нашу первую точку останова на return оператор. Это дает нам возможность найти struct file, возвращающий
дескриптор файла, который получает наш процесс PoC.

Код

source

Действительные кешированные данные находятся на одной или нескольких страницах в физической памяти. Каждая страница физической памяти описывается файлом struct page. Расширяемый массив(struct xarray), содержащий указатели на эти структуры страниц, можно найти в i_pages поле файла struct address_space.

Код

source

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

========================================================================================================================
      Start addr    |   Offset   |     End addr     |  Size   | VM area description 
========================================================================================================================
...
   ffff888000000000 | -119.5  TB | ffffc87fffffffff |   64 TB | direct mapping of all physical memory (page_offset_base)
...

Ключом к поиску “иголки в стоге сена“ является другая область виртуального адресного пространства ядра.

Vmemmap использует виртуально отображаемую карту памяти для
оптимизации операций pfn_to_page и page_to_pfn. Имеется глобальный
страничный указатель структуры *vmemmap, указывающий на практически
непрерывный массив объектов структуры страницы. PFN – это индекс этого
массива, а смещение страничной структуры из vmemmap – это PFN этой
страницы.

Источник

========================================================================================================================
      Start addr    |   Offset   |     End addr     |  Size   | VM area description
========================================================================================================================
...
   ffffe90000000000 |  -23    TB | ffffe9ffffffffff |    1 TB | ... unused hole
   ffffea0000000000 |  -22    TB | ffffeaffffffffff |    1 TB | virtual memory map (vmemmap_base)
   ffffeb0000000000 |  -21    TB | ffffebffffffffff |    1 TB | ... unused hole
...

В отладчике мы можем убедиться, что адрес struct page, связанный со struct address_space целевого файла, открытого нашим процессом PoC, и правда находится в этом диапазоне.

Код

Ядро реализует преобразование этого адреса в состояние непрерывного отображения всей физической памяти, используя серию макросов, которые скрываются за вызовом
lowmem_page_address/page_to_virt.

Код

При выполнении макросов, обязательно учитывайте свою архитектуру(например, x86) и проверяйте определения времени компиляции в файле .config вашей сборки(например,CONFIG_DYNAMIC_MEMORY_LAYOUT=y ). На значения vmemmap_base и page_ofset_base обычно влияет KASLR(kernel address space layout randomization), но они могут быть определены во время исполнения, например, с помощью отладчика.

Вооружившись этими знаниями, мы можем написать скрипт для отладчика, который сделает этот расчет за нас и выведет кешированные данные из открытого нами файла.

struct page at 0xffffea0004156880
> virtual: 0xffff8881055a2000
> data: b'File owned by root!\n'[...]b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'

Проверка прав доступа к файлу подтверждает, что у нас действительно нет прав на запись в него.

-rw-r--r-- 1 root root 20 May 19 20:15 target_file

Далее мы будем исследовать вторую подсистему ядра, связанную с уязвимостью Dirt Pipe.

Pipes

Как было сказано в начале статьи, каналы(pipes) – это механизм однонаправленного межпроцессного взаимодействия(IPC), используемый в UNIX-подобных операционных системах. По сути, канал – это буфер в пространстве ядра, к которому процессы обращаются через файловые дескрипторы. Однонаправленность означает, что существует два типа файловых дескрипторов – для чтения и для записи:

write() ---> pipefds[1] │>>>>>>>>>>>>>>>>>>>│ pipefds[0] ---> read()

При создании канала, вызывающий процесс получает оба файловых дескриптора, но обычно он извлекает выгоду путем распространения одного или обоих файловых дескрипторов другим процессам(например, с помощью fork/clone или через сокеты домена UNIX)для облегчения процесса IPC. Как пример, они используются оболочками для подключения stdout и stdin запущенных процессов.

Код

В контексте статьи мы будем рассматривать анонимные каналы, но также существуют именованные каналы, о которых тоже было бы полезно знать.

Еще есть отличная книга “The Linux Programming Interface”, написанная Майклом Керриском (Michael Kerrisk). В главе 44 “Pipes and FIFO” развернуто представлена тема каналов.

Pipes (инициализация)

После открытия целевого файла, исполнение нашего PoC продолжается созданием канала:

int pipefds[2];
...
pause_for_inspection("About to create pipe()");
if (pipe(pipefds)) {
    exit(1);
}

Давайте посмотрим, что делает ядро, чтобы обеспечить функциональность канала.

Наш системный вызов обрабатывается функцией ядра do_pipe2.

SYSCALL_DEFINE1(pipe, int __user *, fildes)
{
    return do_pipe2(fildes, 0);
}

source

Код

source

Как мы можем видеть, здесь создаются два целочисленных файловых дескриптора, поддерживаемые двумя разными файлами. Один для чтения(fd[0]) и один для записи(fd[1]). Дескрипторы также копируются из ядра в пространство пользователя copy_to_user(fildes, fd, sizeof(fd)), где fildes – это указатель пространства пользователя, который мы установили при вызове pipe(pipefds) в нашем PoC.

После вызова __do_pipe_flags() видно, какие структуры данных использует ядро для реализации канала. Мы объединили соответствующие структуры и их взаимодействия на следующей схеме:

Схема

Два целочисленных файловых дескриптора, представляющие канал в пользовательском пространстве, поддерживаются двумя структурными файлами, которые отличаются только своими битами разрешений. Также, они оба ссылаются на один и тот же struct inode.

Inode(индексный узел) – это структура данных в файловой системе, которая описывает объект файловой системы, такой как файл или каталог. Каждый индексный узел хранит атрибуты и расположение дисковых блоков данных объекта. Атрибуты объекта файловой системы могут включать в себя метаданные(время последнего изменения, доступа, модификации), а также данные о владельце и правах доступа. Каталог – это список индексных узлов с присвоенными им именами. Список включает запись для себя, своего родителя и каждого из своих дочерних элементов.

Источник

Поле i_fop индексного узла содержит указатель на struct file_operations. Эта структура содержит указатели функций для реализации различных операций, которые могут выполняться в канале. Важно отметить, что они включают в себя функции, которые ядро будет использовать для обработки запроса процесса на чтение или запись канала.

Код

source

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

Специфическая для канала часть индексного узла(inode) в основном содержится в функции struct pipe_inode_info, на которую указывает поле i_pipe.

Код

source

На этом этапе мы уже можем сформировать первое представление о том, как реализованы каналы. На высоком уровне ядро представляет для себя канал как кольцевой массив pipe_buffer структур, иногда также называемый “кольцом”(ring). Поле bufs является указателем на начало этого массива.

Код

source

В этом массиве есть две позиции: одна для записи в «заголовок»(head), и одна для чтения из «хвоста»(tail) канала. ring_size по умолчанию равен 16 и всегда будет степенью 2, поэтому цикличность реализуется путем кеширования доступа к индексу с помощью ring_size – 1 (например, bufs[head & (ring_size – 1)]). Поле страницы является указателем на struct page, которая описывает, где хранятся фактические данные, содержащиеся в pipe_buffer. Ниже мы подробнее остановимся на процессе
добавления и использования данных. Обратите внимание, с каждым pipe_buffer связана одна страница. Это означает, что общая емкость канала равна ring_size * 4096 байт(4 кб).

Процесc может получить и установить размер «кольца»(ring) с помощью системного вызова fcntl() с флагами F_GETPIPE_SZ и F_SETPIPE_SZ соответственно. Для простоты, наш PoC устанавливает размер канала равным одному буферу.

Код

source

Анализ исходного кода ядра

Мы также можем следить за настройкой канала в исходном коде ядра. Инициализация целочисленных файловых дескрипторов происходит в __do_pipe_flags().

Код

source

Файлы поддержки инициализируются в функции create_pipe_files(). Как мы можем заметить, оба файла идентичны с точки зрения разрешений, а также содержат ссылку на канал в своих конфиденциальных данных, и представляются как потоки.

Код

source

Инициализация общей структуры inode(индексного узла) происходит в функции get_pipe_inode(). Мы видим, что создается inode, а также информация для канала выделяется и сохраняется таким образом, что inode–>pipe впоследствии можно использовать для доступа к каналу из данного нам inode. Кроме того, inode–>i_fops указывает выполнения, используемые для файловых операций в канале.

Код

source

Большинство специфичных для канала настроек происходит в alloc_pipe_info(). Здесь мы можем уви