1 puntos por GN⁺ 2024-03-27 | 1 comentarios | Compartir por WhatsApp
  • CVE-2024-1086 en nf_tables del kernel de Linux genera una doble liberación de sk_buff por una falla de validación de entrada en los verdicts de Netfilter y, si se dan las condiciones, puede derivar en una escalada local de privilegios
  • El flujo de ataque hace que, durante el procesamiento de NF_DROP, un skb ya liberado siga procesándose como si fuera NF_ACCEPT, de modo que el mismo objeto se libera nuevamente en una ruta posterior
  • El PoC fue validado en KernelCTF mitigation, Debian, Ubuntu y kernels vanilla; como mínimo, el rango v5.14.21~v6.6.14 queda dentro del impacto según la kconfig, y la corrección se distribuyó en las ramas stable en febrero de 2024
  • La clave es Dirty Pagedirectory, una técnica KSMA solo de datos que asigna de forma duplicada páginas PTE y PMD a la misma página física para acceder a direcciones físicas arbitrarias solo con lecturas/escrituras desde espacio de usuario
  • Al combinar búsqueda de KASLR físico, bypass de modprobe_path, ejecución sin archivos y enlace de descriptores de archivo para escapar de namespaces, se convierte en un caso práctico de LPE que apunta a la vez a la gestión de memoria del kernel y al subsistema de red

Condiciones de la vulnerabilidad y alcance del impacto

  • El bug de nf_tables fue registrado como CVE-2024-1086, y su punto central es una falla de validación de entrada en el manejo de verdicts de Netfilter del kernel de Linux, donde se permite un drop error positivo
  • El exploit requiere las siguientes condiciones
    • nf_tables debe estar habilitado
    • Los namespaces de usuario sin privilegios deben estar habilitados
    • En distribuciones principales como Debian y Ubuntu, el hecho de que esta configuración venga habilitada por defecto es una premisa del ataque
  • Según las pruebas, las ramas stable linux-5.15.y, linux-6.1.y y linux-6.6.y están dentro del impacto, y linux-6.7.1 también podría estarlo
  • En febrero de 2024 se distribuyó la corrección del bug en las ramas stable
  • El código fuente del PoC está publicado en el repositorio del PoC de CVE-2024-1086

Flujo de código que produce la doble liberación

  • Un verdict de Netfilter es un valor que decide si un paquete se descarta, se acepta, se envía a una cola, etc.
  • El flujo vulnerable comienza porque nft_verdict_init() no restringía lo suficiente el valor de verdict ingresado por el usuario, lo que permitía configurar un valor que parecía NF_DROP pero cuyo drop error era positivo
  • nf_hook_slow() mira los bits inferiores del verdict y, si lo interpreta como NF_DROP, primero libera el skb con kfree_skb_reason()
  • Luego, si el resultado de NF_DROP_GETERR() devuelve un valor correspondiente a NF_ACCEPT, el llamador considera que el paquete fue aceptado y continúa el procesamiento
  • Como resultado, un skb ya liberado se vuelve a liberar en una ruta posterior, creando un primitivo de double-free

Objeto corrompido y forma de procesamiento de paquetes

  • La doble liberación afecta a struct sk_buff en skbuff_head_cache y al objeto sk_buff->head
  • sk_buff->head contiene el contenido real del paquete y, según el tamaño del paquete IPv4, puede asignarse desde kmalloc-256 hasta páginas order 4 del buddy allocator
  • El PoC está diseñado para usar paquetes IP grandes y entrar por la ruta del buddy allocator, no por la del slab allocator
  • La cola de fragmentación IPv4 se usa para retrasar la segunda liberación del skb o inducirla en el momento deseado
  • Si en la ruta de paquetes se usan campos de un skb corrompido, el kernel puede entrar en pánico; por eso se evita la pila TCP/UDP y se usa una ruta específica de error de fragmentos IP

Alcance de pruebas y tasa de éxito

  • Se probaron varias versiones y configuraciones de kernel en entornos de kernel vanilla, KernelCTF, Debian y Ubuntu
  • Los casos exitosos incluyen los siguientes entornos
    • Linux v5.14.21, v5.15.148, v5.16.20, v5.17.15, v5.18.19, v5.19.17, v6.0.19
    • Linux v6.1.55 de KernelCTF Mitigation v3
    • Linux v6.1.69 en Debian Bookworm 6.1.0-17
    • Linux v6.1.72 de KernelCTF LTS
    • Ubuntu Jammy v6.2.0-37
    • Linux v6.2.16, v6.3.13
  • Los casos fallidos incluyen v5.4.270, v5.10.209, v6.4.16, Ubuntu Jammy v6.5.0-15, v6.5.13, v6.6.14, v6.7.1, entre otros
  • Algunos fallos posteriores a v6.4.0 están relacionados con la detección bad_page() provocada por CONFIG_INIT_ON_ALLOC_DEFAULT_ON=y
  • En un entorno v6.4.16, la tasa de éxito fue de 99.4% y, según el caso, bajó hasta 93.0%; el tamaño de muestra fue n=1000 en ambos casos

Forma de corrección

  • La primera corrección propuesta podía introducir un breaking change en medio del stack de Netfilter
  • La corrección del maintainer de Netfilter restringe con mayor rigor, a nivel de API, los verdicts que vienen desde la entrada del usuario
  • El parche rechaza los parámetros de verdict DROP/QUEUE provenientes de entradas de userland
  • Según la descripción del CVE, nft_verdict_init() permitía valores positivos como drop error dentro del hook verdict, y nf_hook_slow() podía generar una doble liberación cuando NF_DROP y NF_ACCEPT se superponían
  • El parche de corrección puede consultarse en [PATCH nf] netfilter: nf_tables: reject QUEUE/DROP verdict parameters.

Dirty Pagedirectory y acceso arbitrario a memoria física

  • La técnica central del PoC es Dirty Pagedirectory, una variante de la técnica existente Dirty Pagetable
  • La idea principal es asignar de forma duplicada una página PTE y una página PMD a la misma página física
  • Si se escribe en una región de direcciones virtuales un valor que parece un valor PTE, otra región de direcciones virtuales lo interpreta como una entrada de tabla de páginas y mapea la página física indicada
  • Este método funciona como un kernel-space mirroring attack, que permite acceder a direcciones físicas arbitrarias configurando direcciones y flags de permisos únicamente con lecturas/escrituras de direcciones en espacio de usuario
  • Se usa para evadir mitigaciones como virtual KASLR, KPTI, SMAP, SMEP y CONFIG_STATIC_USERMODEHELPER

Manipulación del asignador de páginas

  • En la asignación de páginas del kernel intervienen el slab allocator, el buddy allocator y el PCP allocator
  • En tamaños grandes, el skb head usa páginas order 4 del buddy allocator, pero las páginas PTE/PMD son páginas order 0, por lo que no encajan directamente
  • Para resolverlo se usan dos métodos de page conversion
    • PCP list draining: colocar páginas order 4 en la freelist del buddy y vaciar la freelist order 0 de PCP para que el buddy allocator la rellene de nuevo con páginas order 0
    • race condition: aprovechar una carrera durante la segunda liberación para colocar una página order 4 en la freelist order 0
  • PCP list draining es el método más simple, estable y rápido
  • El método basado en race condition se usó en el exploit inicial para KernelCTF, pero quedó obsoleto porque dependía de entornos con gran latencia en la TTY serial, como una VM QEMU

Bypass de KernelCTF mitigation

  • En el entorno de KernelCTF mitigation, la mitigación que hubo que evadir activamente fue el chequeo de corrupción de la freelist de sk_buff
  • Como skbuff_head_cache->offset == 0x70, el puntero next de la freelist se superpone con skb->len
  • Tras la primera liberación del skb, skb->len se sobrescribe con parte del puntero de la freelist y, durante el parseo posterior de paquetes, este valor puede cambiar y activar el chequeo de corrupción
  • La detección se evade liberando un skb normal adicional sobre el skb corrompido para sobrescribir la cabeza de la freelist
  • Un desarrollador de KernelCTF consideró que esta evasión podría mitigarse si también se verifica, al momento de liberar, el puntero next de la cabeza de la freelist

TLB flush y búsqueda de KASLR físico

  • Cuando Dirty Pagedirectory modifica las tablas de páginas de una forma inesperada, en la caché TLB de la CPU pueden quedar traducciones antiguas
  • Desde espacio de usuario se hace un fork() y luego el hijo ejecuta munmap() y queda durmiendo para hacer flush de la TLB
  • Se confirmó que este método funcionó al 100% en CPUs AMD y VMs QEMU
  • La búsqueda de KASLR físico reduce el espacio de búsqueda aprovechando que la dirección base física del kernel está alineada con CONFIG_PHYSICAL_START o CONFIG_PHYSICAL_ALIGN
  • Suponiendo 8 GiB de memoria física y alineación de 16 MiB, hay 512 candidatos, que se identifican generando una firma de base del kernel con el script get-sig

modprobe_path y obtención de una root shell

  • Después de obtener lectura/escritura arbitraria de memoria física, el PoC escanea un rango de unos 80 MiB después de la base del kernel para encontrar modprobe_path
  • En configuraciones normales, busca el patrón "/sbin/modprobe" con padding nulo y valida la variable real comprobando si se refleja en /proc/sys/kernel/modprobe
  • Si CONFIG_STATIC_USERMODEHELPER está habilitado, apunta a la cadena "/sbin/usermode-helper"
  • Para obtener una root shell, sobrescribe modprobe_path o la cadena del static usermode helper con una ruta memfd del estilo /proc/<pid>/fd/<fd>
  • El script de privilege escalation conecta los descriptores de archivo del exploit con stdin/stdout de la shell, de modo que funcione tanto en una terminal local como en una reverse shell

Ejecución sin archivos y composición del PoC

  • El PoC admite fileless execution, sin escribir archivos en disco
  • Si el objetivo tiene Perl, se puede usar memfd_create() para cargar el binario del exploit en memoria y ejecutarlo como /proc/$$/fd/<fd>
  • Las dependencias de compilación son libnftnl-dev y libmnl-dev
  • La build estática para KernelCTF usó musl-gcc, una elección para evitar tanto el enlazado estático con glibc como el problema de opcodes AVX512 de QEMU
  • El código fuente del exploit está dividido en varios archivos y, al tener como objetivo un binario ejecutable independiente, ante errores opta por crash/exit en lugar de devolver error codes

Estabilidad y limitaciones

  • Como el estado de la pagetable del proceso del exploit puede volverse inestable, después de un éxito o fallo se reduce la inestabilidad del kernel dejando al proceso hijo en estado sleep en lugar de finalizarlo
  • Si hay actividad de red, se introduce ruido en la freelist de skb y eso puede afectar la estabilidad
  • En entornos SSH o reverse shell, se reduce la salida por stdout cerca del momento del double-free para minimizar asignaciones/liberaciones de skb causadas por la red
  • En algunas pruebas de hardware, el sistema crasheó unos segundos después; como los frames WiFi también usan skb, es posible que la actividad WiFi haya influido
  • Al deshabilitar el adaptador WiFi desde la BIOS, el exploit funcionó correctamente en ese entorno

Conclusiones del proceso de investigación

  • El PoC se refinó con el objetivo de lograr amplia compatibilidad, alta estabilidad y ejecución sigilosa
  • Además de 2 meses de desarrollo, se dedicaron otros 2 meses a mejorar estabilidad y compatibilidad
  • El exploit en sí no depende mucho del comportamiento del slab allocator, sino que se basa en funciones ampliamente habilitadas como el subsistema IPv4 y la memoria virtual
  • El bug inicial requiere un user namespace sin privilegios y nftables, pero técnicas como Dirty Pagedirectory y PCP draining pueden aprovecharse también en otros exploits reales
  • Este trabajo queda como un caso que profundiza simultáneamente en el subsistema de networking y el subsistema de memory management del kernel de Linux

1 comentarios

 
GN⁺ 2024-03-27
Comentarios en Hacker News
  • Hoy publicaron un exploit de prueba de concepto para CVE-2024-1086, y funciona en Debian y Ubuntu, entre otros
    Las versiones afectadas son Linux kernel v5.14~v6.6, y la compatibilidad con v6.4~v6.6 depende de la configuración del kernel CONFIG_INIT_ON_ALLOC_DEFAULT_ON
    Este bug fue parchado en febrero de 2024, así que hay que actualizar los equipos Linux

    • La fuente está buena y el trabajo es excelente. Me da curiosidad ver qué reacción más grande genera
      https://github.com/Notselwyn/CVE-2024-1086/blob/main/src/mai...
    • Si en la etapa posterior al exploit ya obtuvieron una primitiva parecida a KSMA, me pregunto por qué igual usaron el método de modprobe_path e incluso añadieron fuerza bruta del pid por el enfoque fileless
      Por ejemplo, me gustaría preguntar por qué no eligieron algo como parchear el .text del kernel con shellcode corto para volverse root y escapar del namespace
    • Me pregunto cuáles son las rutas de ataque e impactos posibles de esta vulnerabilidad
    • En el texto dice que las versiones afectadas por el exploit van de Linux kernel v5.14 a v6.4, pero en la página enlazada dice de v5.14 a v6.6
      Aun así, indica que quedan excluidas las ramas parchadas v5.15.149>, v6.1.76>, v6.6.15>
  • En el parche aparece esta frase: “This reverts commit e0abdadcc6e1. [...] Its not clear to me why this commit was made.”
    Me pregunto si alguien investigó el historial de contexto de ese commit
    [1] https://lore.kernel.org/all/20240120215012.129529-1-fw@strle...

    • El commit original ya tiene más de 10 años, así que probablemente quedó enterrado por el paso del tiempo
      Busqué en la vieja lista de netdev, pero no encontré nada, y también podría haber sido un parche enviado directamente al committer Pablo Neira Ayuso
      El autor original era Patrick McHardy, que ahora es una figura evitada, y a menos que Pablo lo recuerde o lo encuentre en sus correos, parece difícil aclarar el caso de uso exacto solo con una investigación básica
    • El commit relacionado es este: https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/lin...
      netfilter: nf_tables: accept QUEUE/DROP verdict parameters, que permite al espacio de usuario especificar un número de cola o un código errno para los veredictos QUEUE y DROP
  • Este texto también es muy impresionante desde la perspectiva de la escritura de seguridad
    Al escribir blogs de seguridad, siempre surge la duda de “cuánto conocimiento previo y de contexto asumir”, y lograr un equilibrio entre accesibilidad y profundidad es difícil
    Como define bien a su público objetivo y ofrece suficiente contexto, me pareció excelente, así que lo guardé para dárselo como material guía a investigadores en formación que conozco cada año

  • Este exploit depende del acceso a user namespaces sin privilegios: sysctl kernel.unprivileged_userns_clone = 1
    Es el valor por defecto en los kernels de Debian/Ubuntu y Arch Linux, y si no necesitas cosas como ejecutar comandos de Docker sin sudo, probablemente conviene desactivarlo

    • Esta configuración no se usa solo para Docker o Pacman
      También está relacionada con el sandbox de Chrome que usan apps Electron o servicios como 1Password, y el binario auxiliar del sandbox también puede funcionar como programa setuid
      Tampoco sorprendería que Proton empiece a usar user namespaces en el futuro, así que en Linux de escritorio general puede que sea mejor no desactivarlo
      En servidores o en Linux endurecido para casos específicos, muchas veces no es mala idea desactivarlo
    • Esta configuración es buena en el sentido de que permite que la gente ejecute contenedores y cosas parecidas sin privilegios de root, pero igual da mucha lástima que al final haya terminado asociada repetidamente con vulnerabilidades de escalada a root
    • El problema real no es esa configuración en sí, sino que los namespaces originalmente son un mecanismo para reducir privilegios
      El problema es que, para que los contenedores “simplemente funcionen”, hacen falta muchísimos hacks del lado de red, y el principal punto de venta de Docker casi que consiste en encargarse de esos hacks peligrosos por ti
      Al final, cualquier solución de contenedores termina aceptando esos hacks, y aunque la funcionalidad normal de namespaces reduce el acceso, el kernel vuelve a abrir la puerta por culpa de los hacks de red
    • En 6.1.65 no aparece esa opción, así que me pregunto si le cambiaron el nombre
  • No entiendo por qué los user namespaces sin privilegios vienen activados por defecto
    Incluso si algo corre dentro de un namespace “sin privilegios”, me cuesta ver por qué por defecto se le da al usuario la capacidad de ejecutar cosas como iptables o mount

    • Los user namespaces sin privilegios permiten que programas sin privilegios configuren un sandbox
      Por ejemplo, Chrome usa namespaces para implementar su sandbox de procesos, pero instala un binario setuid-root para no depender de namespaces sin privilegios
      Como los binarios setuid-root son en sí mismos un riesgo de seguridad, a largo plazo sería preferible que Chrome no tuviera que instalar ese tipo de binarios
      Pero para eso los user namespaces sin privilegios tendrían que estar ampliamente disponibles, y bugs como este terminan retrasando ese futuro
      Además, programas como Chrome, que montan sandboxes basados en namespaces, suelen usar también seccomp para impedir que el código dentro del sandbox use funciones inusuales del kernel como los propios namespaces
      En un escritorio de un solo usuario, la ventaja de imponer con fuerza la separación entre usuario y root no es tan grande, y la mayoría de las cosas interesantes ya son accesibles desde la cuenta del usuario
      En cambio, los sandboxes tipo Chrome sí son esenciales para la seguridad del escritorio, así que en escritorios Linux de un solo usuario tiendo a pensar que habilitar user namespaces sin privilegios mejora la seguridad general
      En sistemas multiusuario, claramente es otra historia
  • Si no siguieran apareciendo errores como este, los espacios de nombres de usuario sin privilegios habrían sido una excelente función de seguridad
    Por ejemplo, sería bueno que ejecutar Flatpak no requiriera binarios setuid del host para aislar aplicaciones

    • El problema es la filosofía de “simplemente funciona”
      La mayoría de los valores predeterminados no son seguros y requieren conocimiento técnico y trabajo de endurecimiento
  • Commit que introdujo el problema: https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/lin...
    En un switch/case anidado, se ve extraño que haya un return en la ruta predeterminada y luego una estructura que espera un fall-through posterior

    • Me pregunto si fue la misma persona
      https://lwn.net/Articles/882397/
    • Si se cambia el enlace al formato de GitHub, queda así: https://github.com/torvalds/linux/commit/f342de4e2f33e0e3916...
      https://bugs.launchpad.net/bugs/cve/2024-1086
      Una vulnerabilidad de use-after-free en el componente netfilter: nf_tables del kernel de Linux permite elevación local de privilegios
      La función nft_verdict_init() permite valores positivos como drop error dentro de una decisión de hook, y como resultado nf_hook_slow() puede procesar NF_DROP con un drop error que parece NF_ACCEPT, lo que puede generar una vulnerabilidad de doble liberación
      Se recomienda actualizar a una versión posterior a f342de4e2f33e0e39165d8639387aa6c19dff660
  • Me pregunto cómo siguen siendo posibles exploits como este aun con mitigaciones modernas como ASLR
    En una clase de la universidad, nos dieron varios binarios para una versión específica de Ubuntu y la tarea era encontrar errores como use-after-free o buffer overflow y explotarlos, y fue realmente difícil
    Encontrar el fallo ya era complicado, y escribir el shellcode exacto para hacer algo útil con ese fallo era todavía más difícil
    En etapas más avanzadas, también estaban activadas mitigaciones como ASLR y stack canaries, y hasta en un entorno estudiantil controlado se sentía casi imposible
    Al final, hay que 1) encontrar un fallo explotable y 2) hallar el payload binario exacto que haga algo útil sin simplemente hacer crashear el programa, y en el mundo real debe ser aún más difícil, así que me pregunto cómo lo logran
    [0] https://web.stanford.edu/class/archive/cs/cs107/cs107.1194/a...

    • Quien tomó esa clase era un principiante con poca experiencia, y en un curso de 10 a 15 semanas tuvo que encontrar y explotar varios bugs
      Mientras además llevaba otras materias, probablemente solo podía dedicar de unas horas a unos días por bug
      Zerodium paga 50 mil dólares por una elevación local de privilegios típica en Linux, y tomando como referencia el costo de desarrolladores de exploits experimentados, eso compra más o menos 200 horas-persona
      Es decir, los expertos están invirtiendo entre 10 y 100 veces más tiempo que un estudiante principiante
      Basta pensar en la diferencia entre alguien que entra por primera vez a una carpintería y un carpintero, entre un principiante en cerámica y un artesano, o entre un pintor novato y un artista profesional
      Y además con 10 a 100 veces más tiempo disponible
      [1] https://zerodium.com/program.html
    • Las mitigaciones modernas han hecho que explotar sea mucho más difícil, pero los investigadores siguen encontrando maneras de evadirlas
      Existen técnicas “clásicas” para evadir la mayoría de las protecciones modernas y, si no las hay, aparecen nuevos ataques o nuevas formas de bypass
      Por ejemplo, para bypass de protecciones del heap se puede ver how2heap[0], también hubo casos de exploits que evadieron KASLR[1], y este exploit parece usar la técnica de dirty pagetable[2]
      Es una estructura continua de juego del gato y el ratón: se agregan mitigaciones y los investigadores encuentran cómo saltárselas
      [0] https://github.com/shellphish/how2heap
      [1] https://www.willsroot.io/2022/12/entrybleed.html
      [2] https://pwning.tech/nftables/#452-the-technique
    • En resumen, hay gente muy buena, gente con suerte y gente muy buena que además tiene suerte
      Para encontrar cosas así, basta con una persona del último tipo
      Hoy en día es realmente difícil, y aun desactivando mitigaciones sigue sin ser fácil encontrar vulnerabilidades y escribir exploits
      Aun así, muchas de las personas que buscan este tipo de fallos trabajan en equipo, paralelizan el fuzzing y pueden combinar conocimientos y otros exploits, o encadenarlos
      La experiencia y el talento de algunos investigadores son sorprendentes, y en este campo tener años o décadas de experiencia vale muchísimo
    • Hice una tarea parecida en una clase similar, donde te daban binarios y tenías que encontrar bugs, y también fue difícil
      Pero si pensamos que muchos de los bugs importantes que se descubren hoy los encuentran equipos bien financiados o actores estatales, resulta más fácil entender que puedan volcar enormes cantidades de personal y recursos para evadir mitigaciones existentes
    • La entrada del blog enlazada en el repositorio tiene una sección aparte sobre KASLR
  • Según Ubuntu, todas las versiones LTS están afectadas, y ya fue corregido en los kernels parcheados actuales: https://ubuntu.com/security/CVE-2024-1086
    Focal fue corregido en 5.4.0-174.193, Jammy en 5.15.0-101.111 y Mantic en 6.5.0-26.26
    Si usas soporte extendido, también incluye a Xenial y Bionic

  • Lo probé en un sistema Debian vulnerable y no logré la escalada de privilegios, pero en el segundo intento todo el sistema se quedó congelado
    El primer intento simplemente falló, así que aun así vale totalmente la pena dedicar tiempo a aplicar el parche

  • La configuración actual del kernel se puede revisar en archivos como /boot/config o /proc/config.gz