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 (файловый дескриптор) — это число, которое однозначно идентифицирует открытый файл в ОС.
Когда программа запрашивает открытие файла, ядро в свою очередь:
- Предоставляет доступ.
- Создает запись в глобальной таблице файлов.
- Предоставляет программе расположение этой записи.
Когда процесс делает успешный запрос на открытие файла, ядро возвращает файловый дескриптор, который указывает на запись в глобальной файловой таблице ядра.
Запись таблицы файлов содержит информацию, такую как:
- Индекс файла
- Смещение в байтах
- Ограничение доступа для данного потока данных (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(). Она пытается получить файл в желаемом ей режиме, и, если это получается, устанавливает новый файловый дескриптор, который поддерживается файлом, и возвращает его в виде целого числа.
Код
Выполнение вызова do_filp_open() связано с риском потеряться в “джунглях” (виртуальной) файловой системы, поэтому мы поместим нашу первую точку останова на return оператор. Это дает нам возможность найти struct file, возвращающий
дескриптор файла, который получает наш процесс PoC.
Код
Действительные кешированные данные находятся на одной или нескольких страницах в физической памяти. Каждая страница физической памяти описывается файлом struct page. Расширяемый массив(struct xarray), содержащий указатели на эти структуры страниц, можно найти в i_pages поле файла struct address_space.
Код
В последнем комментарии дается подсказка, как найти реальную страницу физической памяти в виртуальном адресном пространстве ядра. Дело в том, что ядро отображает всю физическую память в свое виртуальное адресное пространство , поэтому мы знаем, где она находится. Дополнительные сведения см. в документации.
========================================================================================================================
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);
}
Код
Как мы можем видеть, здесь создаются два целочисленных файловых дескриптора, поддерживаемые двумя разными файлами. Один для чтения(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. Эта структура содержит указатели функций для реализации различных операций, которые могут выполняться в канале. Важно отметить, что они включают в себя функции, которые ядро будет использовать для обработки запроса процесса на чтение или запись канала.
Код
Как указано выше, индексный узел не ограничивается описанием каналов, и для других типов файлов это поле будет указывать на другой набор указателей функций.
Специфическая для канала часть индексного узла(inode) в основном содержится в функции struct pipe_inode_info, на которую указывает поле i_pipe.
Код
На этом этапе мы уже можем сформировать первое представление о том, как реализованы каналы. На высоком уровне ядро представляет для себя канал как кольцевой массив pipe_buffer структур, иногда также называемый “кольцом”(ring). Поле bufs является указателем на начало этого массива.
Код
В этом массиве есть две позиции: одна для записи в «заголовок»(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 устанавливает размер канала равным одному буферу.
Код
Анализ исходного кода ядра
Мы также можем следить за настройкой канала в исходном коде ядра. Инициализация целочисленных файловых дескрипторов происходит в __do_pipe_flags().
Код
Файлы поддержки инициализируются в функции create_pipe_files(). Как мы можем заметить, оба файла идентичны с точки зрения разрешений, а также содержат ссылку на канал в своих конфиденциальных данных, и представляются как потоки.
Код
Инициализация общей структуры inode(индексного узла) происходит в функции get_pipe_inode(). Мы видим, что создается inode, а также информация для канала выделяется и сохраняется таким образом, что inode–>pipe впоследствии можно использовать для доступа к каналу из данного нам inode. Кроме того, inode–>i_fops указывает выполнения, используемые для файловых операций в канале.
Код
Большинство специфичных для канала настроек происходит в alloc_pipe_info(). Здесь мы можем увидеть непосредственно создание канала(не только inode, но и pipe_buffers/pipe_inode_info->bufs, которые содержат данные канала).
Код
Отладчик
Мы можем вывести информацию о только что инициализированнном канале(после изменения его размера) путем создания прерывания в конце функции pipe_fcntl(), которая является обработчиком, вызываемом в случае F_SETPIPE_SZ(оператора switch внутри do_fcntl()).
Код
Пока что эта информация нам не понадобится, но в будущем мы сможем ей воспользоваться.
Pipes (reading/writing)
Writing
После выделения канала, PoC продолжает запись в него.
Код
Глядя на файловые операции индексного узла(inode), мы видим, что запись в канал обрабатывается функцией pipe_write(). Когда данные перемещаются между ядром и пользовательским пространством, часто приходится сталкиваться с векторизированным вводом-выводом с использованием объектов iov_eter. В контексте наших целей. мы можем воспринимать их, как буферы, но при желании вы можете узнать о них больше.
Код
При обработке записи(write()) в канал, поведение ядра можно разделить на два сценария. Сначала, оно проверяет, есть ли возможность добавить данные(или хотя бы какую-то их часть ) к странице pipe_buffer, которая в данный момент времени является главной частью ring-а. Ядро узнает это путем проверки трех вещей:
- Пуст ли канал, когда мы начинаем процесс записи(подразумевается наличие
доступных инициализированных буферов, !was_empty) - Установлен ли флаг PIPE_BUF_FLAG_CAN_MERGE. buf->flags &
PIPE_BUF_FLAG_CAN_MERGE - Достаточно ли места осталось в странице. offset + chars <= PAGE_SIZE
Если ответ на все эти пункты положительный, ядро начинает запись, добавляя данные к существующей странице.
Чтобы завершить оставшуюся часть записи, ядро перемещает заголовок(head) к следующему pipe_buffer, выделяет для него новую страницу, инициализирует флаги (устанавливается флаг PIPE_BUF_FLAG_CAN_MERGE, если только пользователь явно не запрашивает, чтобы канал находился в режиме O_DIRECT) и записывает данные в начало новой страницы. Это продолжается до тех пор, пока не останется данных для записи (или не заполнится канал).Что касается режима O_DIRECT для pipe():
[...]
O_DIRECT (since Linux 3.4)
Create a pipe that performs I/O in "packet" mode. Each
write(2) to the pipe is dealt with as a separate packet,
and read(2)s from the pipe will read one packet at a time.
[...]
Эта обработка происходит в if-условии is_packetized(filp) в pipe_write() (см. выше).
Мы также можем наблюдать эти два типа записи в отладчике. Первая запись выполняется в пустой канал и, таким образом, инициализирует наш ранее заполненный нулями буфер канала.
Код
Все остальные записи идут по “пути добавления” и заполняют существующую страницу.
Код
Reading
Затем наш PoC «опустошает» канал, считывая все “A” с конца.
Код
Сценарий, когда процесс запрашивает у ядра чтение(read()) из канала, обрабатывается функцией pipe_read():
Код
Если канал не пустой, данные берутся из tail-indexed буфера pipe_buffer. В случае, если буфер «опустошается» во время чтения, исполняется указатель функции release ops поля в pipe_buffer. Для pipe_buffer, который был инициализирован более ранней функцией write(), поле ops является указателем на struct pipe_buf_operations anon_pipe_buf_ops.
static const struct pipe_buf_operations anon_pipe_buf_ops = {
.release = anon_pipe_buf_release,
.try_steal = anon_pipe_buf_try_steal,
.get = generic_pipe_buf_get,
};
Код
Код
Таким образом, исполняется функция anon_pipe_buf_release(), которая вызывает put_page() для того, чтобы освободить нашу ссылку на страницу. Обратите внимание, что хотя указатель ops установлен на NULL, чтобы сигнализировать об освобождении
буфера, поля page и flags в pipe_buffer остаются неизменными. Из этого выходит, что ответственность за код, который может повторно использовать буфер канала для инициализации всех его полей, лежит на нем. Мы можем это зафиксировать, если посмотрим на структуры каналов после последней read операции.
Код
Резюмируя.
Для нас ключевыми выводами является:
- Записи в канал могут добавляться к странице pipe_buffer-а, если установлен флаг PIPE_BUF_FLAG_CAN_MERGE.
- Этот флаг установлен по умолчанию для буферов, которые инициализируются записью.
- «Опустошение» канала с помощью read() оставляет флаги pipe_buffers без изменений.
Однако запись в канал не единственный способ его заполнения.
Pipes (splicing)
Помимо чтения и записи, программный интерфейс Linux также предполагает системный вызов splice, предназначенный для перемещения данных из канала или в него. И именно с ним наш PoC работает далее:
pause_for_inspection("About to splice() file to pipe");
if (splice(tfd, 0, pipefds[1], 0, 5, 0) < 0) {
exit(1);
}
Но поскольку этот системный вызов может быть не так известен как другие, давайте кратко обсудим его с точки зрения пользователя. Ниже приведен отрывок из документации для разработчиков Linux.
Код
Итак, как упоминалось выше, процесс может получить дескриптор файла, используя системный вызов sys_open. Если процесс хочет записать содержимое файла(или его часть) в канал, у него есть разные варианты сделать это. Он может производить операцию чтения(read()) данных из файла в буфер своей памяти(или mmap() файла), а затем записать(write()) их в канал. Однако весь этот процесс включает в себя три контекстных переключения в kernel-user-space. Чтобы вся эта операция выполнялась
более эффективно, ядро Linux использует системный вызов sys_splice. По сути, он выполняет копирование(на самом деле это является не совсем копированием, это можно будет увидеть ниже) из одного файлового дескриптора в другой в пространстве ядра. Как мы скоро увидим, в этом есть немалый смысл, поскольку содержимое файла или канала уже присутствует в памяти ядра в виде буфера, страницы или другой структуры. Либо fd_in, либо fd_out должен являтьcя каналом.
Другой «fd» может быть сокетом, файлом, блочным устройством, символьным устройством или другим каналом. Вы можете вновь обратиться к статье Макса Келлерманна, где показан пример того, как splicing используется для оптимизации ПО(и как это самое ПО привело к обнаружению описанной им ошибки :). Также полезным опытом будет прочтение данной статьи, в которой Линус Торвальдс объясняет работу системного вызова splice.
Реализация системного вызова splice
Идея реализации splice проиллюстрирована на рисунке ниже. После splicing’а и канал, и кеш страниц имеют разные представления одних и тех же данных в памяти.
Для того, чтобы убедиться, что представленная выше схема верна, мы начнем с точки входа системного вызова SYSCALL_DEFINE6(splice,…) и первым делом перейдем к функции __do_splice(), которая отвечает за копирование из userspace и в userspace. Вызываемая функция do_splice() определяет, что именно мы хотим сделать: присоединиться к каналу, установить соединение с/из/между каналами. В первом случае нижеприведенная функция вызывается:
static long do_splice_to(struct file *in, loff_t *ppos,
struct pipe_inode_info *pipe, size_t len,
unsigned int flags);
и выполняет:
in->f_op->splice_read(in, ppos, pipe, len, flags);
С этого момента, execution path зависит от типа файла, который мы хотим “соединить” с каналом. Поскольку нашей целью является обычный файл, а имеющаяся виртуальная машина использует файловую систему ext2, реализация системного вызова splice находится в ext2_file_operations.
const struct file_operations ext2_file_operations = {
...
.read_iter = ext2_file_read_iter,
...
.splice_read = generic_file_splice_read,
...
};
Примечание: если у вас есть желание, вы можете более подробно разобраться Linux VFS, которая хорошо описана в официальной документации к Linux.
Вызов generic_file_splice_read() наконец приводит нас к filemap_read(). Заметьте, что в этот момент мы переключаемся с файловой системы fs/ на подсистему управления памятью ядра(memory managment subsystem of the kernel) mm/. Дополнительно.
Код
В этой функции происходит фактическое копирование(без реального побайтового копирования) данных из кеша страниц в канал. В цикле данные копируются частями, с помощью вызова функции copy_folio_to_iter(). Обратите внимание, что folio это не совсем то же самое, что и страница, но для наших целей это не столь важно.
copied = copy_folio_to_iter(folio, offset, bytes, iter);
Но что если мы внимательно посмотрим на реализацию данной операции в copy_page_to_iter_pipe()? Мы заметим, что на самом деле данные не копируется вообще!
Код
Сначала мы пытаемся “присоединить” текущую операцию копирования к предыдущей, попробовав увеличить длину pipe_buffer в начале. В случае если это невозможно, мы просто “двигаем” заголовок(head), и помещаем ссылку на страницу, которую мы копируем, в поле данной страницы, убедившись, что длина и смещение установлены правильно. Идея эффективности sys_splice заключается в том, чтобы реализовать ее как операцию с нулевым копированием(zero-copy), где вместо дублирования данных используются указатели(pointers) и счетчики ссылок(reference counts).
Стоит отметить, что этот код имеет возможность повторно использовать pipe_buffers(buf = &pipe->bufs[i_head & p_mask]), поэтому все поля должны быть проверены и, при необходимости, повторно инициализированы, так как есть вероятность существования некоторых старых значений, которые могут оказаться неправильными. К этому в том числе относится отсутствие инициализации флагов, о котором в своем исследовании упоминал Макс Келлерманн.
Мы также можем наблюдать эффект операции нулевого копирования(zero-copy operation) и отсутствия инициализации, воспользовавшись отладчиком.
Это состояние канала до splicing-a,
Код
а это после
Код
Указатель данных в struct_address_space и pipe_buffer в заголовке(head) одинаковы, а длина и смещение отражают то, что наш PoC указал в вызове splice. Обратите внимание, что мы повторно используем буфер, который мы ранее очистили, заново инициализируя все поля, кроме флагов.
Так в чем же заключается проблема?
На данном этапе суть уязвимости становится очевидна. В случае с анонимными буферами каналов разрешается продолжить запись там, где предыдущая была остановлена, на что указывает флаг PIPE_BUF_FLAG_CAN_MERGE. С файловыми буферами(file-backed buffers), созданными с помощью splicing’a, это должно быть запрещено ядром, поскольку эти страницы «принадлежат» страничному кешу, а не каналу.
Таким образом, когда мы производим операцию splicing’a данных из файла в канал, необходимо установить buf->flags=0, чтобы запретить добавление данных к уже существующей, но не полностью записанной, странице(buf->page), так как она принадлежит страничному кешу(т. е. файлу). Когда мы снова производим операцию pipe_write()(или просто write()) в нашей программе, мы записываем в страницу page cache’a, потому что проверка buf->flags & PIPE_BUF_FLAG_CAN_MERGE возвращает true(см. pipe_write).
Итак, главная проблема заключается в том, что мы начинаем с анонимного канала, который затем будет «преобразован»(не весь, только некоторые буферы) в file-backed канал с помощью splice(), но сам канал этой информации не получает, поскольку buf >flags значение не установлено на “0”. Из этого следует, что слияние все еще разрешено.
Патч для устранения уязвимости заключается в добавлении недостающей инициализации.
Код
Как мы можем видеть выше, наш PoC организовал установку флага
PIPE_BUF_FLAG_CAN_MERGE для буфера канала, повторно используемого для
соединения. Таким образом, последняя запись вызовет ошибку.
pause_for_inspection("About to write() into page cache");
if (write(pipefds[1], "pwned by user", 13) != 13) {
exit(1);
}
Возвращаясь к отладчику, мы видим, что финальный вызов pipe_write() добавляется к
частично заполненному pipe_buffer.
Код
Также, мы видим, что содержимое файла было перезаписано с “owned by root” на
“pwned by user” в кеше страницы.
В терминале мы можем подтвердить, что произошло изменение для всех процессов в
системе.
user@lkd-debian-qemu:~$ ./poc
user@lkd-debian-qemu:~$ cat target_file
File pwned by user!
user@lkd-debian-qemu:~$ exit
root@lkd-debian-qemu:~# echo 1 > /proc/sys/vm/drop_caches
[ 232.397273] bash (203): drop_caches: 1
root@lkd-debian-qemu:~# su user
user@lkd-debian-qemu:~$ cat target_file
File owned by root
Также можно отметить, что изменения данных в кеше страницы не записываются на диск. После очистки кеша содержимое файла возвращается в изначальное состояние.
Но, при этом, все программы будут использовать модифицированную версию из страничного кеша, поскольку ядро будет предлагать вам использовать кешированную версию данных файла, так как это прямая задача страничного кеша.
Ограничения
Существуют некоторые ограничения на записи, которые мы можем выполнять с использованием этой техники, связанные с реализацией каналов и страничного кеша, о которых также упоминает в своем исследовании Макс Келлерманн:
- У атакующего должны быть права на чтение, так как необходимо выполнить
“копирование” страницы в канал с помощью splice(). - Смещение не должно быть на границе страницы, поскольку по крайней мере
один байт этой страницы должен быть “скопирован” в канал, опять же, с
помощью splice(). - Запись не может “пересекать” границу страницы, поскольку будет создан новый
анонимный буфер. - Размер файла не может быть изменен, так как канал самостоятельно управляет
заполнением страницы и не сообщает страничному кешу, сколько данных было
добавлено.
Вывод
Детальный анализ любой ошибки может создавать впечатление ее незначительности. Однако это совсем не так. Для понимания ошибки и причины ее возникновения требуется концептуальное понимание множества взаимодействующих между собой подсистем, в нашем случае ядра Linux. Анализ первопричины без PoC, патча или статьи с разбором уязвимости был бы довольно сложной задачей. Стоит также отметить, что разбор данной уязвимости дает отличную возможность дополнить свои знания о ядре Linux и посмотреть, как все работает «под капотом». В заключение, справедливо будет заметить, что, в отличие от подобных уязвимостей, эксплуатация изученной нами сегодня, довольно тривиальна, стабильна и, ко всему прочему, работает в огромном количестве дистрибутивов Linux. Будем надеяться, что данная статья пробудила в вас желание изучить какие-нибудь более сложные уязвимости/ эксплоиты, или погрузиться в исходный код ядра Linux :).