
Caso de estudio educativo y guía de desarrollo de exploits para CVE-2022-0185, un desbordamiento de búfer en el heap del kernel de Linux que permite escalada local de privilegios. Incluye POC, depuración con QEMU y exploit para Ubuntu con análisis técnico detallado.
Este estudio de caso es el resultado de una tarea de ECE 9069: Introducción al Hacking: https://whisperlab.org/introduction-to-hacking/
CVE-2022-0185 es una vulnerabilidad de desbordamiento de búfer basado en montón que se encontró en la forma en que la función legacy_parse_param en la funcionalidad de Contexto de Sistema de Archivos (Filesystem Context) del kernel de Linux verificaba la longitud de los parámetros suministrados. Un usuario local sin privilegios (en caso de que los espacios de nombres de usuario sin privilegios estén habilitados; de lo contrario, necesita el privilegio CAP_SYS_ADMIN dentro de un espacio de nombres) capaz de abrir un sistema de archivos que no admita la API de Contexto de Sistema de Archivos (y por lo tanto recurre al manejo heredado) podría usar esta falla para escalar sus privilegios en el sistema. [1]
Después de que se reportó esta vulnerabilidad, se lanzó un parche para corregir este error:
https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=722d94847de2
https://ubuntu.com/security/CVE-2022-0185#impact-score
Hay un artículo detallado del explorador: https://www.hackthebox.com/blog/CVE-2022-0185:_A_case_study
En este repositorio explicaré los pasos básicos y la información de antecedentes relacionada para reproducir esta vulnerabilidad. Además, si hay algo que no te quede claro, puedes enviarme un correo electrónico a [email protected]. Estaré encantado de responder tus preguntas.
La vulnerabilidad CVE-2022-0185 se publicó el 02/11/2022, con una puntuación base CVSS 3.x de 8.4 (Alta).[1] Esta vulnerabilidad es un desbordamiento de búfer basado en montón, causado por un desbordamiento por debajo de entero sin signo (unsigned integer underflow).
La vulnerabilidad se introdujo en el kernel de Linux v5.1, afectando a todas las distribuciones de Linux con versiones de kernel superiores a 5.1. Por ejemplo, Ubuntu 20.04 LTS (focal) era vulnerable a este error. Sin embargo, se lanzó un parche y está disponible desde la versión 5.4.0-96.109.[3]
Explotar esta vulnerabilidad permite que un usuario local sin privilegios escale sus privilegios en el sistema, comprometiendo potencialmente todo el sistema.[1][2] Aquí hay un análisis detallado de la puntuación CVSS: Puntuación Base: 8.4, lo que indica un riesgo de seguridad significativo que requiere atención inmediata. Puntuación de Impacto: 5.9, sugiriendo un daño potencial sustancial si se explota. Los valores altos de confidencialidad, integridad y disponibilidad contribuyen a esta puntuación. Puntuación de Explotabilidad: 2.5, sugiriendo una explotabilidad relativamente alta. El vector de ataque local, la alta integridad y los altos valores de disponibilidad contribuyen a esta puntuación.
La Tabla 1.1 y la Tabla 1.2 proporcionan más información sobre estas puntuaciones y sus componentes.
| Severidad CVSS v3.1 | Valor |
|---|---|
| Puntuación Base | 8.4 ALTA |
| Puntuación de Impacto | 5.9 |
| Puntuación de Explotabilidad | 2.5 |
Tabla 1.1 Puntuaciones de Severidad CVSS[1]
Tabla 1.2 Vector CVSS[1]
En las computadoras modernas existen dos tipos de enteros: con signo y sin signo. La representación de números con signo generalmente implica una operación llamada complemento a dos.[4] "El complemento a dos utiliza el dígito binario con el mayor valor posicional como signo para indicar si el número binario es positivo o negativo".[4]
La introducción del complemento a dos convierte el cálculo de la resta en una suma, simplificando así el diseño y la implementación de la CPU. La generación del complemento a dos de un entero involucra tres pasos:[4]
La Fig 2.1.1.1 muestra el proceso de conversión en un diagrama con un ejemplo real de conversión de "-6" a su formato de complemento a dos.

Fig 2.1.1.2 Suma Usando Complemento a Dos
La Figura 2.1.1.2 muestra el proceso de sumar el complemento a dos de '-6' a '+6'. Esto demuestra cómo el uso del complemento a dos permite que la suma se utilice como sustituto de la resta.

Fig 2.1.1.2 Suma Usando Complemento a Dos
En la Sección 2.1.1, ya entendemos qué es el complemento a dos. Ahora, veamos el escenario del desbordamiento por debajo de números sin signo en las computadoras. En las computadoras modernas, al usar números sin signo, el bit más significativo no se trata como un bit de signo; en cambio, es parte del número sin signo en sí. Esta situación significa que al realizar una resta con un número sin signo, debemos tener cuidado, ya que puede llevar a una condición conocida como desbordamiento por debajo de número sin signo.[5]
La Fig 2.1.2.1 ilustra la situación de restar 6 de 5 para un número sin signo de 8 bits. El resultado final es 255 debido al envolvimiento del número sin signo. Cuando este desbordamiento por debajo ocurre en una declaración condicional, tiene el potencial de interrumpir la funcionalidad de la declaración.

Fig 2.1.2.1 Desbordamiento por Debajo de Número Sin Signo
En el kernel de Linux, el Asignador de Slabs (Slab Allocator) es un mecanismo de gestión de memoria utilizado para la asignación y liberación eficiente de pequeños fragmentos de memoria. Ofrece rendimiento al mantener varios cachés de Slabs, cada uno conteniendo bloques de memoria de tamaño fijo. Típicamente, kmalloc-32 asigna 32 bytes de memoria, es un slab kmalloc-32, mientras que kmalloc-4k asigna 4096 bytes de memoria, es un slab kmalloc-4k.[6]
Además, la asignación de slabs en el kernel de Linux típicamente implica asignar memoria desde un espacio de direcciones contiguo dentro de la región de memoria heap del kernel. Esta dirección contigua es gestionada por el kernel y se utiliza para asignar memoria para varios objetos y estructuras de datos del kernel. La Fig 2.2.1.1 muestra la disposición de los slabs en la memoria del kernel de Linux.

Fig 2.2.1.1 Asignador de Slabs en Linux [7] (El autor de esta figura es https://leviathan.vip/)
Si deseas reproducir el proceso con un kernel de Linux compilado por ti mismo, lee los siguientes archivos markdown para obtener la información de antecedentes:
Nota:
Todos los archivos markdown, así como el código y los scripts, están en diferentes carpetas de este repositorio. Cada carpeta tiene su propio archivo markdown. ¡Léelo antes de intentar hacer algo!
En la sección 2.1, explicamos cómo funciona el desbordamiento por debajo sin signo. Ahora, examinaremos la función del kernel que contiene esta vulnerabilidad.
El usuario "clubby789" descubrió una vulnerabilidad en la función del kernel legacy_parse_param. Esta función es principalmente responsable de analizar los parámetros pasados al kernel. En CVE-2022-0185, se invocaba después de usar fsopen para abrir un descriptor de archivo, seguido del uso de la función fsconfig para pasar pares clave-valor de configuración al kernel. Una versión simplificada de legacy_parse_param se muestra en el siguiente código. [2]```c
static int legacy_parse_param(struct fs_context *fc, struct fs_parameter *param) {
struct legacy_fs_context *ctx = fc->fs_private; // [1]
unsigned int size = ctx->data_size; // [2]
size_t len = 0;
int ret;
[ ... ]
switch (param->type) {
case fs_value_is_string:
len = 1 + param->size; // [3]
case fs_value_is_flag:
len += strlen(param->key);
break;
default:
return invalf(fc, "VFS: Legacy: Parameter type for '%s' not supported", param->key);
}
if (len > PAGE_SIZE-2-size) return invalf(fc, "VFS: Legacy: Cumulative options too large"); // [4]
[ ... ]
if (!ctx->legacy_data) {
ctx->legacy_data = kmalloc(PAGE_SIZE, GFP_KERNEL); // [5]
if (!ctx->legacy_data) return -ENOMEM;
}
ctx->legacy_data[size++] = ','; // [6]
len = strlen(param->key);
memcpy(ctx->legacy_data + size, param->key, len);
size += len;
if (param->type == fs_value_is_string) {
ctx->legacy_data[size++] = '=';
memcpy(ctx->legacy_data + size, param->string, param->size);
size += param->size;
}
ctx->legacy_data[size] = '\0';
ctx->data_size = size;
ctx->param_type = LEGACY_FS_INDIVIDUAL_PARAMS;
return 0;
}
Del fragmento de código anterior, podemos ver que las líneas [1] y [2] establecen el contexto del código, mientras que la línea [4] contiene la declaración donde ocurre el desbordamiento inferior sin signo. La línea [5] maneja la asignación de bloques del montón, y las líneas [6] y [7] se encargan de poblar los datos en el bloque asignado. En particular, la línea [6] añade una coma (',') como delimitador separado, y también se añade un signo igual ('='), lo que resulta en dos bytes adicionales más allá del tamaño real de los datos.
En la línea [4], las variables dentro de la sentencia if contienen PAGE_SIZE (una macro establecida en 4096) y size (un número sin signo de 64 bits). Cuando un número sin signo acumula hasta 4095, se produce un desbordamiento inferior, lo que hace que la sentencia if siempre se evalúe como falsa. Esto permite una escritura fuera de los límites en el bloque vecino. El desbordamiento inferior es causado por restar un número sin signo, lo que resulta en 4096 - 4095, lo que da como resultado un número sin signo de 18446744073709551615.[2]
El resultado de `18446744073709551615` se explica en el siguiente texto:```c
if (len > PAGE_SIZE-2-size) return invalf(fc, "VFS: Legacy: Cumulative options too large");
Note que, aquí PAGE_SIZE es igual a 4096 bytes, y el 2 representa los caracteres , y = añadidos para separar cada par clave-valor. El problema es que size es un valor sin signo, por lo que cuando size alcanza 4095, la expresión PAGE_SIZE-2-size será igual a valor con signo: -1, pero para valor sin signo: 18446744073709551615 debido al complemento a dos, como se muestra en el siguiente diagrama! [3]

Por lo tanto, la sentencia if anterior siempre será falsa, lo que significa que el resto de los datos se copiarán al montón más allá del slab que asignamos!
Después de entender cómo puede ocurrir este desbordamiento de número sin signo, podemos proceder a construir un código de prueba de concepto (POC) para demostrar la vulnerabilidad.
El usuario “clubby789” nos proporciona un código POC detallado, que se muestra en el siguiente fragmento de código. El código es conciso; primero abre un descriptor de archivo llamado ext4, luego usa fsconfig varias veces para poblar datos al kernel.
Dos cosas a tener en cuenta aquí:
En la sección 3.1.1, mencionamos que se deben poblar 4095 bytes de datos antes de observar la escritura fuera de límites. Dado que cada ciclo solo pobla 35 bytes, debemos realizar la operación 117 veces (4095 / 35) antes de observar la memoria dinámica para completar la prueba de concepto.
### 3.1.3 POC con QEMU
En este repositorio: GitHub - dcheng69/CVE-2022-0185-Case-Study, hemos proporcionado un script de shell llamado poc.sh para facilitar el proceso de depuración. Lea el archivo markdown en la carpeta Poc antes de comenzar la configuración.
Dado que legacy_parse_param es una función del kernel, necesitará depurar una función del kernel. Para ello, debe compilar el código fuente del kernel para obtener los símbolos y el código fuente necesarios. También hemos proporcionado un archivo markdown detallado para guiarlo en el proceso. Consulte la carpeta Compile_linux para más detalles.
En la Fig. 3.1.3.1, demostramos que después de poblar 4095 bytes de datos en el montículo del kernel, logramos desencadenar una escritura fuera de límites explotando un desbordamiento de enteros sin signo (unsigned underflow). Además, poblamos un total de 4130 bytes de datos en un slab kmalloc-4k, corrompiendo con éxito el slab vecino. Aunque en este ejemplo el slab vecino no contiene información (todo ceros), podemos construir cuidadosamente nuestro código para aprovechar esta característica y escribir datos maliciosos. Demostraremos cómo lograrlo en la sección 3.2 Exploit.

**Fig 3.1.3.1 POC con QEMU**
Para más detalles, consulte https://github.com/dcheng69/CVE-2022-0185-Case-Study/blob/main/Poc/poc.md en la carpeta `Poc` de este repositorio.
## 3.2 Exploit
Después de conocer esta vulnerabilidad, podemos proceder a explotarla. Los detalles están documentados en la carpeta `exploit-ubuntu` y en este archivo markdown: https://github.com/dcheng69/CVE-2022-0185-Case-Study/blob/main/explot-ubuntu/exploit.md
En resumen, compilamos el código fuente de Ubuntu, obtuvimos el archivo deb, luego lo probamos en una máquina virtual para obtener una versión específica del kernel. Luego modificamos el desplazamiento del código de explotación para apuntar a la versión del kernel utilizando la información obtenida del archivo `System.map`. Finalmente, actualizamos grub y reiniciamos para explotar.
### 3.2.1 Resumen del Exploit
El usuario 'clubby789' nos proporciona un código de explotación detallado. Comenzaremos con un resumen, seguido de explicaciones de varios conceptos clave mediante diagramas. Finalmente, presentaremos los resultados de la explotación utilizando Ubuntu ejecutándose en una máquina virtual (VirtualBox).
Después de demostrar la prueba de concepto de esta vulnerabilidad, ahora podemos proceder a explotarla. En la Fig. 3.2.1.1, se ilustra un resumen de cómo explotar esta vulnerabilidad:
- La sección izquierda se centra en obtener la dirección base del kernel de Linux. Esto se logra explotando el desbordamiento de enteros sin signo para sobrescribir el campo m_ts de la estructura msg_msg, permitiendo leer fuera de límites y acceder a estructuras del kernel previamente rociadas.
- La sección derecha tiene como objetivo obtener privilegios de root. Esto se logra usando el desbordamiento de enteros sin signo para sobrescribir el puntero next de la estructura msg_msg, apuntándolo a modprobe_path. Luego desencadenamos un fallo de página que invoca nuestro código fuse construido, permitiendo escrituras arbitrarias en el espacio del kernel.

**Fig 3.2.1.1 Resumen del Exploit**
### 3.2.2 Obtener la Dirección Base del Kernel de Linux
Como se analizó anteriormente, explotaremos el desbordamiento de enteros sin signo en esta parte para sobrescribir el campo m_ts de la estructura msg_msg, permitiendo una lectura fuera de límites. Al rociar el montículo con estructuras que contienen punteros del kernel, podemos esperar obtener una fuga de memoria.
La estructura msg_msg es una estructura de datos en el kernel de Linux utilizada para implementar colas de mensajes System V. En esta sección, nos centramos en la estructura interna de struct msg_msg y la lógica de las funciones relacionadas con el envío, recepción y asignación de mensajes. Como se muestra en la Fig. 3.2.2.1, estas son las funciones que debemos entender.
La implementación del envío de mensajes se encuentra en el archivo msg.c, que define la longitud máxima de un mensaje como 8192 bytes. En la función alloc_msg, los mensajes se dividen en segmentos según su longitud. Si la longitud del mensaje, junto con el encabezado del mensaje, supera una página (4096 bytes), el mensaje se almacenará en varios segmentos enlazados mediante punteros.

**Fig 3.2.2.1 Envío y recepción de struct msg_msg**
En la Fig. 3.2.2.2, podemos ver que struct msg_msg actúa como un encabezado de mensaje, ocupando 0x30 bytes de memoria. Si hay datos residuales en el mensaje, se almacenarán en segmentos de mensaje y se vincularán a struct msg_msgseg. Por lo tanto, si el kernel permite mensajes de hasta un máximo de 8192 bytes, los datos se almacenarán en un máximo de tres segmentos de mensaje.

**Fig 3.2.2.2 struct msg_msg**
En la Fig. 3.2.2.3, ilustramos la estructura de struct msg_msg. Por el código, sabemos que el campo m_ts es el que debemos sobrescribir para leer fuera de límites. (¡Puede encontrar el archivo fuente draw.io en la carpeta `res` si lo necesita!)

**Fig 3.2.2.3 Estructura de struct msg_msg**
Ahora que entendemos la estructura de struct msg_msg, necesitamos aprender cómo obtener una fuga del kernel. El kernel de Linux tiene una característica de Aleatoriedad del Espacio de Direcciones del Kernel (KASLR), lo que significa que el código del kernel se carga en una dirección aleatoria decidida durante la fase de arranque. Sin embargo, el desplazamiento desde el punto de inicio del kernel hasta cualquier dirección de función permanece constante, lo que nos permite realizar operaciones específicas para llenar el espacio del montículo con estructuras que contienen funciones particulares del kernel. Al disminuir el desplazamiento, podemos encontrar la dirección de inicio del kernel.
Afortunadamente, podemos rociar fácilmente el montículo con estructuras seq_operations abriendo /proc/self/stat, que reside en slabs kmalloc-32. La definición de seq_operations se muestra en la Fig. 3.2.2.3.

**Fig 3.2.2.3 Estructuras para la fuga del kernel**
Finalmente, el proceso general se representa en la Fig. 3.2.2.4. Comenzamos poblando legacy_data con 4095 bytes de datos para preparar la sobrescritura. Luego, construimos mensajes usando struct msg_msg. Debido a que la memoria del montículo se asigna de forma continua, los mensajes construidos probablemente estarán adyacentes al slab kmalloc-4k vecino. Sobrescribimos el campo m_ts controlando los datos que escribimos en legacy_data.
A continuación, rociamos el montículo con múltiples estructuras seq_operations de kmalloc-32. Luego recibimos datos de la cola de mensajes, lo que desencadena una lectura fuera de límites. Ajustando el desplazamiento desde la función del kernel, podemos obtener la dirección base del kernel.

**Fig 3.2.2.4 Resumen de la lectura fuera de límites**
### 3.2.3 Obtener Privilegios de Root
Similar a nuestro análisis anterior, en esta parte necesitamos configurar un sistema de archivos FUSE, que permita que nuestro código de espacio de usuario maneje fallos de página desde el espacio del kernel. Al mismo tiempo, sobrescribiremos el puntero msg_msgseg *next para que apunte a modprobe_path, permitiendo escrituras arbitrarias en el espacio del kernel.
Primero examinemos la pila de llamadas de FUSE representada en la Fig. 3.2.3.1. En general, FUSE nos permite implementar un sistema de archivos en el espacio de usuario. Cuando hay una nueva operación, el sistema invocará el código que definimos para FUSE.

**Fig 3.2.3.1 Resumen de FUSE**
Analicemos cómo usar FUSE para lograr escrituras arbitrarias en el espacio del kernel. Primero, consideremos la operación de envío de mensajes para struct msg_msg. Esta operación implica escribir datos en el espacio del kernel. Si construimos el mensaje con dos segmentos, podemos sobrescribir el puntero y escribir en cualquier dirección del kernel. Este concepto se ilustra en la Fig. 3.2.3.2.
Además, después de examinar la lógica de envío de mensajes a la cola, sabemos que el proceso implica copiar un búfer del espacio de usuario al espacio del kernel. Si los mensajes son lo suficientemente largos, se copiarán segmento por segmento. Si podemos desencadenar un fallo de página durante este proceso, podemos lograr el escenario mostrado en la Fig. 3.2.3.2.
Afortunadamente, FUSE proporciona la funcionalidad necesaria. Podemos mapear una página a FUSE, y cuando se desencadene un fallo de página, el sistema llamará a nuestra función de lectura de FUSE para manejar el fallo. Al pausar el proceso de lectura hasta que el desbordamiento de enteros sin signo sobrescriba el puntero msg_msg_seg *next y luego reanudar el proceso de envío de mensajes, podemos lograr escrituras arbitrarias. El proceso completo se demuestra en la Fig. 3.2.3.3.

**Fig 3.2.3.2 Usando envío de mensajes**

**Fig 3.2.3.3 FUSE y envío de mensajes**
### 3.2.4 Explotación con Virtual Box
Siguiendo el archivo markdown de: https://github.com/dcheng69/CVE-2022-0185-Case-Study/blob/main/explot-ubuntu/exploit.md
Una vez completada la compilación, puede observar los resultados de la explotación en la Fig. 3.2.4.1.

**Fig 3.2.4.1 Explotación en Virtual Box**
# 4. Mitigación de la Vulnerabilidad
## 4.1 Parches Oficiales
Después de que se reportara esta vulnerabilidad, Linux y muchas de sus distribuciones incorporaron parches para corregir este error.[9] [10] [11]
En la siguiente captura de pantalla, mostramos el parche incorporado por Linus Torvalds. La mitigación de este problema de desbordamiento consiste simplemente en convertir la operación de resta en suma.

# 5. Impacto en el Mundo Real
En este informe, demostramos cómo esta vulnerabilidad puede ser explotada para comprometer un sistema Ubuntu. Además, podría afectar potencialmente a sistemas antiguos que carecen de actualizaciones de seguridad periódicas.
En cuanto a Kubernetes, esta vulnerabilidad podría resultar en escalada de privilegios, escape de contenedores o ataques de denegación de servicio.
Aunque no se han reportado pérdidas causadas por esta vulnerabilidad en las noticias, subraya la importancia de aplicar consistentemente las actualizaciones de seguridad críticas. El investigador que reportó esta vulnerabilidad ejemplifica las prácticas de hacking ético que todos deberíamos esforzarnos por mantener.[12]
# Referencias
[1] https://nvd.nist.gov/vuln/detail/CVE-2022-0185
[2] https://www.hackthebox.com/blog/CVE-2022-0185:_A_case_study
[3] https://ubuntu.com/security/CVE-2022-0185#impact-score
[4] [https://en.wikipedia.org/wiki/Two%27s_complement](https://en.wikipedia.org/wiki/Two's_complement)
[5]https://www.gnu.org/software/c-intro-and-ref/manual/html_node/Unsigned-Overflow.html
[6] https://www.kernel.org/doc/gorman/html/understand/understand011.html
[7] https://leviathan.vip/
[8] https://www.willsroot.io/2021/08/corctf-2021-fire-of-salvation-writeup.html
[9] https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=722d94847de2
[10] https://ubuntu.com/security/CVE-2022-0185
[11] https://access.redhat.com/security/cve/CVE-2022-0185
[12]https://jfrog.com/blog/the-impact-of-cve-2022-0185-linux-kernel-vulnerability-on-popular-kubernetes-engines/
[14] https://github.com/chenaotian/CVE-2022-0185?tab=readme-ov-file
[15] https://www.tutorialspoint.com/two-s-complement
| Métricas CVSS v3.1 | Valor |
|---|
| Vector de Ataque (AV) | Local |
| Privilegios Requeridos (PR) | Ninguno |
| Interacción del Usuario (UI) | Ninguno |
| Confidencialidad (C) | Alta |
| Integridad (I) | Alta |
| Disponibilidad (A) | Alta |