2 puntos por GN⁺ 2024-10-27 | 1 comentarios | Compartir por WhatsApp
  • mseal en Linux 6.10 es una syscall de mitigación de exploits que sella regiones de memoria virtual en ejecución, dificultando que un atacante que ya obtuvo permisos de ejecución de código cambie después los permisos o la disposición de las VMA
  • mseal(start, len, flags) agrega VM_SEALED a un rango de VMA alineado a páginas, y el kernel rechaza cambios en rutas como mprotect, munmap, mmap(MAP_FIXED), mremap y algunas rutas destructivas de madvise
  • Mientras que memfd_create o memfd_secret se parecen más al control de acceso para archivos anónimos basados en RAM y memoria secreta, mseal se enfoca en impedir cambios de permisos, desmapeos y remapeos después de que un atacante remoto logra ejecutar código
  • Los principales objetivos defensivos son ataques que eluden NX con mprotect(PROT_EXEC) para ejecutar shellcode, y exploits solo de datos que crean agujeros en la memoria con munmap/remapeo y luego los rellenan con datos del atacante
  • Es posible que se integre a partir de glibc 2.41, pero como la stack y el heap no se sellan automáticamente debido a su expansión y contracción en tiempo de ejecución, los desarrolladores deben elegir con cuidado el momento y el alcance de su aplicación

Qué protección ofrece mseal

  • El sellado de memoria (memory sealing) es una función que vuelve un rango de VMA en ejecución casi inmutable frente a modificaciones peligrosas posteriores
  • Aunque un atacante consiga una primitiva de ejecución de código, en una VMA sellada le resulta difícil cambiar permisos mediante operaciones de memoria virtual o manipular la disposición a su favor
  • El equipo de Chrome Security introdujo esta syscall para soportar la estrategia de CFI de V8 en ChromeOS basado en Linux
  • Tras varias rondas de discusión y reescritura, llegó al kernel de Linux, y su integración con glibc podría ampliar su uso más allá de los navegadores

Diferencias con la familia memfd

  • memfd_create y memfd_secret se parecen más a la familia de sellado de archivos (file sealing)
    • Permiten crear archivos anónimos basados en RAM
    • memfd_secret hace que solo los procesos con el descriptor de archivo puedan acceder a esa región de memoria
    • Puede usarse para mapeos de espacio de usuario estilo “secure enclave” que protegen datos sensibles en memoria
  • mseal es una syscall orientada a la mitigación de exploits contra atacantes remotos que buscan ejecutar código, más que contra atacantes locales que intentan filtrar información sensible

Funcionamiento interno del kernel

  • La firma de la syscall es int mseal(unsigned long start, size_t len, unsigned long flags)
    • start y len indican el inicio y la longitud de la VMA válida que se sellará
    • len debe estar correctamente alineado a páginas
    • Actualmente flags no se usa y debe ser 0
  • La implementación en Linux 6.12 llama a do_mseal
    • current->mm apunta a mm_struct, el espacio completo de direcciones de memoria virtual del proceso que llama
    • mmap dentro de mm_struct contiene la lista de vm_area_struct, regiones de memoria contiguas creadas con mmap
    • Áreas como la stack o VDSO también pueden representarse como una VMA
  • do_mseal calcula la dirección final del rango y luego bloquea el área de memoria con mmap_write_lock_killable
  • check_mm_seal recorre cada VMA del rango objetivo y primero verifica la validez de los límites
    • Si hay memoria no asignada en medio del rango, devuelve -ENOMEM
    • Esta primera comprobación evita que, ante un error, solo algunas VMA queden selladas
  • apply_mm_seal recorre de nuevo el mismo rango y agrega la bandera VM_SEALED a las VMA objetivo mediante mseal_fixup

Operaciones de memoria prohibidas después del sellado

  • El patchset del kernel agrega comprobaciones de VM_SEALED en mm/madvise.c, mm/mmap.c, mm/mprotect.c, mm/mremap.c y mm/mseal.c
  • mprotect y pkey_mprotect llaman internamente a can_modify_vma desde mprotect_fixup, y si la VMA está sellada devuelven -EPERM
  • En una VMA sellada no se permiten las siguientes operaciones
    • Cambiar bits de permisos mediante mprotect o pkey_mprotect
    • Desmapear con munmap
    • Reemplazar un mapeo sellado por un mapeo mutable o no sellado mediante mmap(MAP_FIXED)
    • Expandir o reducir tamaño mediante mremap
    • Mover a un nuevo destino mediante mremap(MREMAP_MAYMOVE | MREMAP_FIXED)
    • Usar madvise con algunas banderas destructivas
  • En movimientos con mremap, la comprobación de sellado se aplica tanto a la VMA de origen como a la de destino
  • Si no está MREMAP_DONTUNMAP, la VMA de origen se desmapea, y en ese caso también debe pasar la comprobación de sellado de munmap
  • En Linux 6.10 o superior se puede usar mseal mediante una llamada directa a syscall, y el wrapper de ejemplo usa el número de syscall 462 y flags=0

Bloquear la ejecución de shellcode reforzando NX

  • Un atacante puede preferir ejecutar shellcode incluso si dispone de técnicas de reutilización de código como ROP
    • Coloca shellcode en una stack o heap no ejecutable
    • Ejecuta una cadena ROP inicial mediante una vulnerabilidad
    • Desactiva el bit NX del área donde está el shellcode con mprotect(PROT_EXEC)
    • Salta a esa área y ejecuta el shellcode
  • El ataque al daemon SMB de MikroTik RouterOS que usa CVE-2018-7445 es un ejemplo de este flujo
    • Coloca shellcode basado en sockets en un heap no ejecutable
    • Una cadena ROP creada mediante un desbordamiento de stack cambia los permisos de la memoria del heap
    • Luego ejecuta el shellcode
  • Si se sella esa VMA con mseal, la comprobación can_modify_vma impide el cambio de permisos cuando se llama a mprotect
  • En el código de ejemplo, si no se sella la página de shellcode en la stack, puede ejecutarse después de mprotect(PROT_READ|PROT_WRITE|PROT_EXEC)
  • Si se sella la misma página con mseal, mprotect no logra cambiar los permisos reales y la ejecución del shellcode provoca una falla de segmentación

Restricciones al aplicarlo a stack y heap

  • Si mseal se introduce a partir de glibc 2.41, el cargador dinámico aplicará sellado a un conjunto predefinido de VMA
  • En el plan actual, la stack y el heap no se sellan automáticamente
  • La stack y el heap pueden expandirse durante la ejecución, por lo que el sellado automático podría romper el comportamiento de la aplicación
    • El asignador de heap puede llamar a la syscall brk para recuperar espacio
    • En esta ruta, la reducción puede ocurrir pasando por arch_unmap y do_vmi_unmap
    • En estado sellado, esos desmapeos no se permiten, por lo que la asignación dinámica de memoria podría romperse
  • Los desarrolladores deben decidir, según el contexto de la aplicación, en qué momento y a qué regiones es seguro aplicar el sellado
  • La macro de ejemplo sella repetidamente las páginas de frames de stack seleccionados que podrían contener datos no confiables
  • Si se vuelve a llamar a mseal sobre una VMA ya sellada, se trata como no-op y no produce error
  • Funciones como la expansión automática de stack o stack splitting requieren atención adicional
  • Si un atacante insiste en usar shellcode, también podría crear una nueva región ejecutable con mmap y copiar el payload desde un área legible, pero el procedimiento se vuelve más engorroso

Mitigación de exploits solo de datos basados en desmapeo

  • Bloquear mprotect también impide que una región sellada pase a ser escribible, lo que ayuda a proteger variables de datos que, si se modificaran, podrían fortalecer una primitiva de exploit
  • Los mantenedores de Chrome consideraron una técnica que pasa punteros dañados a syscalls de desmapeo y remapeo para hacer agujeros en la memoria y rellenarlos de nuevo con datos controlados por el atacante
  • Como este método no cambia directamente transferencias de flujo de control como direcciones de retorno de stack o punteros a funciones, puede eludir garantías de CFI forward-edge y backward-edge
  • En navegadores que implementan compiladores JIT, esta técnica puede ser especialmente útil
    • Turbofan de V8 puede crear regiones que alternan entre RW y RX
    • Un atacante puede hacer que se genere código ejecutable mediante JavaScript en hot paths durante la compilación JIT
    • Puede rellenar una región desmapeada para sobrescribir datos importantes y producir un cambio que lleve a la ejecución de código
  • Es un exploit solo de datos que afecta el flujo de control manipulando datos específicos de la memoria, sin secuestrar directamente el flujo de control ni requerir una filtración previa de punteros
  • mseal puede bloquear estos escenarios de hole-punching al impedir desmapeos y remapeos

Caso House of Muney

  • House of Muney usa una técnica similar basada en desmapeo en exploits de heap en espacio de usuario
  • Qualys usó esta técnica en un exploit real contra un bug antiguo de Qmail
  • La técnica depende de que, cuando los chunks de asignación grandes superan M_MAP_THRESHOLD, malloc y free llamen directamente a mmap y munmap, respectivamente
  • En chunks grandes no hay una caché intermedia de freelist, lo que simplifica el procedimiento de explotación
  • Si se altera el metadato de tamaño en la parte superior de un chunk asignado a otro tamaño de página y luego se llama a free, puede producirse un munmap sobre la región de memoria adyacente al chunk
  • El ejemplo de Dulin apunta a las regiones .gnu.hash y .dynsym mediante un munmap arbitrario
    • Luego las rellena de nuevo con un chunk de mmap más grande
    • Permite sobrescribir una entrada PLT aún no resuelta
    • Revive un ataque estilo GOT overwrite
  • El PoC abreviado asigna dos chunks grandes, manipula el campo de tamaño de top[-1], luego desmapea también la región adyacente con free(top) y la rellena de nuevo con datos X mediante una asignación más grande
  • Esta técnica puede funcionar incluso con CFI y no requiere una filtración previa de ASLR
  • Se espera que el sellado del conjunto de VMA previsto en la integración de mseal con glibc mitigue automáticamente este ataque al proteger el código binario mapeado y las bibliotecas dinámicas frente a trucos de desmapeo y remapeo
  • Los desarrolladores que quieran mayor endurecimiento pueden sellar selectivamente asignaciones mmap que no vayan a expandirse ni desmapearse durante la vida del programa

Alcance futuro de uso

  • mseal es una mitigación relativamente nueva del kernel de Linux, y puede haber más casos de uso aún no cubiertos
  • Cuando la integración con glibc se complete y madure, podrían seguir mejoras ajustadas a los requisitos de la syscall
  • El parámetro flags, que actualmente no se usa, también podría recibir usos concretos en el futuro
  • Al aplicarlo en software real, hay que evaluar tanto la vida útil de la memoria que se sellará como su necesidad de modificación

1 comentarios

 
GN⁺ 2024-10-27
Opiniones de Hacker News
  • Estaría bueno que alguien con conocimiento interno resumiera qué objeciones y preocupaciones hubo en las discusiones intensas de la lista de correo del kernel.
    Suelo evitar la lista de correo en sí porque a veces se pone demasiado picante, y ya tengo suficiente dolor de cabeza.
    El mecanismo en sí parece razonable, pero me sorprende que el kernel no tuviera ya una función así.

    • No sé si hubo más aparte del hilo enlazado, pero en general fue franqueza al estilo Linus.
      Entre los flags propuestos había uno que permitiría ignorar el sello (seal), y Linus reaccionó más o menos en plan: “¿entonces en un lugar no se podrá hacer munmap, pero en otro se podrá ignorar el sello?”.
      Después fue más contundente: “una vez sellado, está sellado”, y no vale eso de que “algunos lugares respetan el sello y otros arbitrarios no”.
      Más tarde llegó a decir que los sellos no se pueden ignorar ni son negociables, y que si seguían proponiendo flags para ignorarlos los pondría en la ignore list.
    • https://lwn.net/ml/linux-kernel/7071.1697661373@cvs.openbsd....
      En un correo de Theo de Raadt a Jeff Xu, ante la afirmación de que los enfoques de mimmutable() y mseal() son distintos, pero que ambos apuntan a hacer más seguras las aplicaciones de Linux sellando la memoria frente a atacantes, respondió: “parece que estás creando mseal solo para Chrome”.
      Consideró que no encajaría bien con las aplicaciones en general, porque es demasiado complejo y porque, según la experiencia con mimmutable(), las aplicaciones no lo gestionan directamente, sino que queda a cargo de execve(), la inicialización de libc y ld.so.
    • Matthew Wilcox también respondió con dureza a Jeff Xu, en el sentido de “gracias por demostrar que no sabes qué hay que impedir”.
      Coincidió con Theo y Linus, y opinó que la idea básica de mimmutable() es buena, pero que la forma en que se dividió e implementó es horrible.
    • Este artículo puede servir: https://lwn.net/Articles/948129/
  • Me da curiosidad cómo se usa esta llamada al sistema.
    Chrome la quiere, pero como un atacante podría volver a mapear con otros flags, las páginas selladas no se pueden desmapear.
    Entonces, para páginas asignadas en tiempo de ejecución, ¿no queda prácticamente inutilizable salvo que uno tenga pensado conservarlas durante toda la vida del proceso?
    Si es así, ¿no sería difícil aplicarla a objetivos muy atractivos como la memoria del sandbox de JS?
    Me pregunto si este tipo de trabajo se resuelve ejecutándolo en un proceso separado, sellando la memoria y luego matando el proceso al terminar.
    No conozco bien la gestión de memoria y procesos de Chrome, así que no estoy seguro de por qué eso no sería un problema, y también me pregunto si esta es la razón por la que a menudo se explica que esta función no es útil para la mayoría de los programas.

    • Aquí los múltiples procesos podrían ser una opción.
      Tengo entendido que Chrome los usa ampliamente, y como de todos modos se necesitan procesos separados por motivos como el aislamiento mediante namespaces, quizá vaya por ese lado.
  • Discusión posterior a mseal(), 20 de octubre de 2023: https://lwn.net/Articles/948129/
    mseal() se acerca, 19 de enero de 2024: https://lwn.net/Articles/958438/
    Sellado de memoria para GNU C Library, 12 de junio de 2024: https://lwn.net/Articles/978010/

  • Es una lástima que el sistema operativo tenga que implementar llamadas así, aun cuando la arquitectura x86_64 moderna tiene muchas funciones que ayudan a la programación y la computación seguras.
    Creo que la actitud de intentar remendar sistemas antiguos construidos sobre legados obsoletos, formas de pensar viejas y paradigmas que no se ajustan al mundo y al conocimiento actuales está frenando el avance de la computación y, literalmente, poniendo en riesgo a miles de millones de personas.
    Por supuesto, no quiero decir que este cambio no sea un paso en la dirección correcta, pero si dejamos de lado el ideal existente de que el sistema operativo debe funcionar como lo hace, y consideramos los sistemas y conocimientos actuales y lo que la gente quiere de los sistemas, podemos imaginar sistemas que liberen a desarrolladores y usuarios de las cargas y riesgos que se les imponen hoy.
    Es cierto que existen bugs de arquitectura, pero como el software ni siquiera aprovecha correctamente las funciones actuales, usar los bugs de arquitectura como contraargumento esquiva el punto central.
    Si se construye sobre cimientos inestables, siempre habrá una forma más barata de comprometer el sistema.

    • Me da curiosidad: ¿qué funciones de seguridad no usadas o poco usadas de x86_64 existen que puedan aprovecharse sin que el sistema operativo tenga una forma de usarlas o activarlas?
      También me interesa saber por qué consideras que mseal no se justifica, y qué sería mejor en su lugar.
  • ¿Se puede sobrescribir o desactivar la llamada al sistema mseal con el truco de LD_PRELOAD?

    • A diferencia de los métodos tradicionales de protección de memoria de Linux, mseal es una llamada al sistema orientada a la mitigación de ataques de ejecución remota de código, más que a frenar a un atacante local que intenta extraer secretos sensibles de la memoria.
      Si un atacante remoto puede cambiar el entorno local, habría que asumir que ya comprometió el sistema.
    • Probablemente no se pueda con LD_PRELOAD.
      Para que tenga efecto, tendría que ser una función importada, pero las llamadas al sistema crudas no se pueden interceptar de esa manera.
      Discusión sobre interceptación de llamadas al sistema en Linux: https://stackoverflow.com/questions/69859/how-could-i-interc...
      Crear uno mismo un kernel parcheado que finja que mseal funciona podría ser la forma más fácil de “desactivar” la función.
      Sin embargo, como un programa que usa mseal puede verificar si realmente funciona, un kernel comprometido además necesitaría alguna forma de desactivar mseal en secreto para impedir las comprobaciones de la app después de aplicarlo.
    • Si se tiene control al inicio del proceso, hay bastantes formas de sobrescribirlo.
      Por ejemplo, se puede rastrear el ejecutable con ptrace para vigilar las llamadas al sistema y hacer que se salte mseal(2).
      Esta llamada al sistema no está pensada para el modelo de amenaza de “el atacante ya tenía acceso antes de que se inicializara el proceso”.
    • Se puede sobrescribir el wrapper de la llamada a mseal, pero no la llamada al sistema en sí.
      Por lo que encontré, las sobrescrituras de llamadas al sistema mediante precarga cambian el wrapper, y si se hace la llamada al sistema directamente, parece que no se podría sobrescribir.
      Técnicamente, quizá se podría sobrescribir la propia función syscall.
    • Según https://lwn.net/Articles/978010/, se planea incorporar un glibc tunable.
  • Meta: el prototipo de mseal() que aparece en el artículo necesita edición.
    El primer argumento aparece como unsigned start addr, pero probablemente lo correcto sea unsigned long start_addr.

    • Ahora parece estar bien: int mseal(unsigned long start, size_t len, unsigned long flags)
  • En “Memory Sealing ‘Mseal’ System Call Merged for Linux 6.10” (2024) https://news.ycombinator.com/item?id=40474510#40474551 ya se había discutido cómo debería CPython dar soporte a la llamada al sistema mseal().

  • OpenBSD tiene esta función desde hace tiempo: https://man.openbsd.org/mimmutable.2
    Me pregunto por qué una función tan obvia recién ahora llega a Linux.

    • mimmutable de OpenBSD se introdujo en OpenBSD 7.3, lanzado el 10 de abril de 2023, así que no es “desde hace tiempo”.
      En cambio, Linux y FreeBSD tienen memfd_create desde hace mucho, y OpenBSD no tiene archivos anónimos, por lo que depende de shm_open.