
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.