
Exploit de prueba de concepto y análisis detallado de la vulnerabilidad de escalada de privilegios CVE-2022-0847 (Dirty Pipe) del kernel de Linux, incluido un entorno Docker para pruebas y depuración.
[toc]
Este artículo se publicó originalmente en la cuenta pública de seguridad de Huawei, esta es la versión del blog (más completa)
Enlace original: https://mp.weixin.qq.com/s/6VhWBOzJ7uu80nzFxe5jpg
ID de vulnerabilidad: CVE-2022-0847 (alias: Dirty Pipe)
Producto vulnerable: kernel de Linux - syscall splice
Versiones afectadas: Linux 5.8 (introducido por el parche f6dd975583bd) ~ 5.16.11, 5.15.25, 5.10.102 (corregido)
Impacto: permite escribir hasta una página de contenido (suficiente) en cualquier archivo legible, pudiendo escalar privilegios localmente.
Docker de análisis de la vulnerabilidad: chenaotian/cve-2022-0847 (si aún no se puede acceder, es porque aún no lo he subido)
Proporciona:
Inicio:
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
Depuración:
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]
El principio básico de la vulnerabilidad es que al llamar a la función
splice, se puede enviar un archivo a lapipemediante "copia cero". A nivel de código, la copia cero consiste en usar directamente la página de caché del archivo (page cache) como páginabufde lapipe. Sin embargo, aquí se introduce un error de inicialización de variable, lo que provoca que la página de caché del archivo sea tratada posteriormente en el canalpipecomo una página de caché normal de lapipe, permitiendo su "reescritura" y, por lo tanto, su modificación. No obstante, en esta situación, el kernel no considera esta página de caché como "sucia" y no la vacía a disco en un corto período de tiempo (hasta el próximo reinicio, por ejemplo). Durante ese tiempo, todos los accesos a ese archivo utilizarán la página de caché modificada, logrando así una operación de "escritura arbitraria en cualquier archivo legible durante un breve período". Esto puede utilizarse para escalar privilegios localmente.
Según el parche, el punto de ocurrencia está en la función copy_page_to_iter_pipe, donde se añadió la inicialización de buf->flags. Por lo tanto, se trata de una vulnerabilidad de variable no inicializada.

El punto de llamada de copy_page_to_iter_pipe se encuentra dentro de la syscall splice. La función splice (llamada al sistema) transporta el contenido de un archivo a la tubería mediante un método de "copia cero". Esto tiene un mejor rendimiento que enviar directamente el contenido del archivo a la tubería. Se explicará con más detalle a continuación.
Primero, para entender la vulnerabilidad "Dirty Pipe", hay que conocer la pipe. La pipe es un canal de comunicación proporcionado por el kernel, creado mediante las funciones pipe/pipe2, que devuelven dos descriptores de archivo: uno para enviar datos y otro para recibirlos, similar a dos extremos de una tubería. No se entrará en más detalles.

Brevemente, en la implementación del kernel, el espacio de caché de la pipe tiene normalmente una longitud total de 65536 bytes, gestionado en páginas: 16 páginas en total (4096 bytes cada una). Las páginas no son contiguas, sino que se gestionan mediante un array, formando una lista circular. Se mantienen dos punteros de lista: uno para escribir (pipe->head) y otro para leer (pipe->tail). Aquí se analiza principalmente la función 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] Si la caché de la pipe no está vacía, intenta continuar escribiendo desde la última página actual
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] Clave: si el flag PIPE_BUF_FLAG_CAN_MERGE está presente, significa que la página permite continuar escribiendo.
* Si la longitud a escribir no cruza el límite de la página, se continúa escribiendo; de lo contrario, se usa una nueva página */
ret = pipe_buf_confirm(pipe, buf);
···
ret = copy_page_from_iter(buf->page, offset, chars, from);
···
}
buf->len += ret;
···
}
}
for (;;) {//[3] Si no se puede continuar en la página anterior, se usa una nueva página
··· ···
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] Solicitar una nueva página
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] Colocar la nueva página en el array de páginas
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] Establecer flag, por defecto PIPE_BUF_FLAG_CAN_MERGE
pipe->tmp_page = NULL;
copied = copy_page_from_iter(page, 0, PAGE_SIZE, from);
//[7] Copiar datos
··· ···
ret += copied;
buf->offset = 0;
buf->len = copied;
··· ···
}
··· ···
}
··· ···
return ret;
}