2 points par GN⁺ 2024-10-27 | 1 commentaires | Partager sur WhatsApp
  • mseal dans Linux 6.10 est un syscall d’atténuation des exploits qui scelle des zones de mémoire virtuelle en cours d’exécution, afin de rendre plus difficile, pour un attaquant ayant obtenu des droits d’exécution de code, la modification ultérieure des permissions ou de l’agencement des VMA
  • mseal(start, len, flags) ajoute VM_SEALED à une plage de VMA alignée sur les pages, et le noyau refuse les changements via mprotect, munmap, mmap(MAP_FIXED), mremap et certains chemins destructeurs de madvise
  • Alors que memfd_create ou memfd_secret relèvent plutôt des fichiers anonymes en RAM et du contrôle d’accès à de la mémoire secrète, mseal se concentre sur le blocage des changements de permissions, des unmapping et des remapping après l’exécution de code par un attaquant distant
  • Les défenses typiques ciblent les attaques qui contournent NX avec mprotect(PROT_EXEC) pour exécuter du shellcode, ainsi que les exploits uniquement fondés sur les données qui créent des trous en mémoire via munmap/remapping puis les remplissent avec des données contrôlées par l’attaquant
  • Une intégration est possible à partir de glibc 2.41, mais la pile et le tas ne sont pas scellés automatiquement à cause de leur extension/réduction à l’exécution ; les développeurs doivent donc choisir avec soin le moment et la portée de l’application

Les protections offertes par mseal

  • Le scellement de mémoire (memory sealing) est une fonctionnalité qui rend une plage de VMA en cours d’exécution presque immuable vis-à-vis des modifications dangereuses ultérieures
  • Même si un attaquant obtient une primitive d’exécution de code, il devient difficile, dans une VMA scellée, de changer les permissions ou de manipuler avantageusement l’agencement via des opérations de mémoire virtuelle
  • L’équipe Chrome Security a introduit ce syscall pour prendre en charge la stratégie CFI de V8 sur ChromeOS basé sur Linux
  • Après plusieurs discussions et réécritures, il a été intégré au noyau Linux, et son périmètre d’utilisation pourrait s’étendre au-delà des navigateurs via une intégration dans glibc

Différences avec la famille memfd

  • memfd_create et memfd_secret relèvent plutôt du scellement de fichiers (file sealing)
    • Ils permettent de créer des fichiers anonymes basés en RAM
    • memfd_secret fait en sorte que seuls les processus possédant le descripteur de fichier puissent accéder à la zone mémoire correspondante
    • Ils peuvent être utilisés pour des mappings en espace utilisateur de style « secure enclave » afin de protéger des données sensibles en mémoire
  • mseal est un syscall conçu comme une atténuation d’exploit contre les attaquants distants cherchant l’exécution de code, plutôt que contre des attaquants locaux visant l’exfiltration d’informations sensibles

Fonctionnement interne du noyau

  • La signature du syscall est int mseal(unsigned long start, size_t len, unsigned long flags)
    • start et len indiquent le début et la longueur de la VMA valide à sceller
    • len doit être correctement aligné sur les pages
    • Pour l’instant, flags n’est pas utilisé et doit valoir 0
  • L’implémentation dans Linux 6.12 appelle do_mseal
    • current->mm pointe vers la mm_struct, c’est-à-dire tout l’espace d’adressage mémoire virtuel du processus appelant
    • Le champ mmap de mm_struct contient la liste des vm_area_struct, les zones de mémoire contiguës créées par mmap
    • Des zones comme la pile ou le VDSO peuvent aussi être représentées comme une VMA
  • do_mseal calcule l’adresse de fin de la plage, puis verrouille la zone mémoire avec mmap_write_lock_killable
  • check_mm_seal parcourt chaque VMA de la plage cible et vérifie d’abord la validité des limites
    • Si de la mémoire non allouée se trouve au milieu de la plage, il renvoie -ENOMEM
    • Cette première vérification sert à éviter qu’une partie seulement des VMA soit scellée en cas d’erreur
  • apply_mm_seal parcourt à nouveau la même plage et ajoute le flag VM_SEALED aux VMA cibles via mseal_fixup

Opérations mémoire interdites après scellement

  • Le patchset du noyau ajoute des vérifications de VM_SEALED dans mm/madvise.c, mm/mmap.c, mm/mprotect.c, mm/mremap.c et mm/mseal.c
  • mprotect et pkey_mprotect appellent en interne can_modify_vma depuis mprotect_fixup, et renvoient -EPERM si la VMA est scellée
  • Les opérations suivantes ne sont pas autorisées sur une VMA scellée
    • Changement des bits de permission via mprotect ou pkey_mprotect
    • Unmapping via munmap
    • Remplacement d’un mapping scellé par un mapping mutable et non scellé via mmap(MAP_FIXED)
    • Augmentation ou réduction de taille via mremap
    • Déplacement vers une nouvelle destination via mremap(MREMAP_MAYMOVE | MREMAP_FIXED)
    • madvise avec certains flags destructeurs
  • Lors d’un déplacement avec mremap, le contrôle de scellement s’applique à la fois aux VMA source et destination
  • Sans MREMAP_DONTUNMAP, la VMA source est unmappée, et elle doit alors aussi passer le contrôle de scellement de munmap
  • À partir de Linux 6.10, mseal peut être utilisé par appel direct au syscall ; l’exemple de wrapper utilise le numéro de syscall 462 et flags=0

Bloquer l’exécution de shellcode en renforçant NX

  • Même avec des techniques de réutilisation de code comme ROP, un attaquant peut préférer exécuter du shellcode
    • Il place du shellcode sur une pile ou un tas non exécutable
    • Il exécute une chaîne ROP initiale via une vulnérabilité
    • Il désactive le bit NX de la zone contenant le shellcode avec mprotect(PROT_EXEC)
    • Il saute vers cette zone et exécute le shellcode
  • L’attaque du daemon SMB de MikroTik RouterOS utilisant CVE-2018-7445 illustre ce déroulement
    • Du shellcode basé sur des sockets est placé dans un tas non exécutable
    • Une chaîne ROP créée via un dépassement de pile change les permissions de la mémoire du tas
    • Le shellcode est ensuite exécuté
  • Si la VMA correspondante est scellée avec mseal, la vérification can_modify_vma empêche le changement de permissions lors de l’appel à mprotect
  • Dans le code d’exemple, si la page de pile contenant le shellcode n’est pas scellée, l’exécution est possible après mprotect(PROT_READ|PROT_WRITE|PROT_EXEC)
  • Si la même page est scellée avec mseal, mprotect ne peut pas réellement modifier les permissions, et l’exécution du shellcode provoque une erreur de segmentation

Contraintes d’application à la pile et au tas

  • Lorsque mseal sera introduit à partir de glibc 2.41, le chargeur dynamique devrait appliquer le scellement à un ensemble prédéfini de VMA
  • Dans le plan actuel, la pile et le tas ne sont pas scellés automatiquement
  • La pile et le tas peuvent s’étendre à l’exécution, si bien qu’un scellement automatique pourrait casser le comportement de l’application
    • L’allocateur du tas peut appeler le syscall brk pour récupérer de l’espace
    • Sur ce chemin, une réduction peut se produire via arch_unmap et do_vmi_unmap
    • En état scellé, ce type d’unmapping n’est pas autorisé, ce qui peut casser l’allocation dynamique de mémoire
  • Les développeurs doivent déterminer, selon le contexte de l’application, à quel moment et sur quelles zones le scellement peut être appliqué
  • L’exemple de macro scelle de manière répétée les pages de certaines frames de pile sélectionnées, susceptibles de contenir des données non fiables
  • Appeler à nouveau mseal sur une VMA déjà scellée est traité comme un no-op et ne produit pas d’erreur
  • Les fonctionnalités comme l’extension automatique de la pile ou le stack splitting nécessitent une attention particulière
  • Si un attaquant tient absolument au shellcode, il pourrait aussi créer une nouvelle zone exécutable avec mmap et copier le payload depuis une zone lisible, mais la procédure devient plus contraignante

Atténuer les exploits uniquement fondés sur les données et basés sur l’unmapping

  • Le blocage de mprotect empêche aussi une zone scellée de devenir accessible en écriture, ce qui aide à protéger les variables de données qui, si elles étaient modifiées, pourraient renforcer une primitive d’exploit
  • Les mainteneurs de Chrome ont envisagé une technique consistant à transmettre des pointeurs corrompus à des syscalls d’unmapping/remapping pour percer des trous en mémoire, puis les remplir à nouveau avec des données contrôlées par l’attaquant
  • Comme cette méthode ne modifie pas directement des transitions de flux de contrôle comme les adresses de retour sur la pile ou les pointeurs de fonction, elle peut contourner les garanties CFI forward-edge et backward-edge
  • Cette technique peut être particulièrement utile dans les navigateurs implémentant un compilateur JIT
    • Turbofan de V8 peut créer des zones basculant entre RW et RX
    • Un attaquant peut faire générer du code exécutable via du JavaScript sur les hot paths pendant la compilation JIT
    • En remplissant une zone unmappée, il peut écraser des données importantes et produire des modifications conduisant à l’exécution de code
  • Il s’agit d’un exploit uniquement fondé sur les données, qui manipule certaines données en mémoire pour influencer le flux de contrôle sans détourner directement le flux de contrôle ni nécessiter de fuite de pointeur
  • mseal peut bloquer ces scénarios de hole-punching en empêchant l’unmapping et le remapping

Exemple House of Muney

  • House of Muney utilise une technique similaire basée sur l’unmapping dans les exploits de tas en espace utilisateur
  • Qualys a utilisé cette technique dans un exploit réel visant un ancien bug de Qmail
  • La technique repose sur le fait que, lorsque de gros chunks d’allocation dépassent M_MAP_THRESHOLD, malloc et free appellent respectivement directement mmap et munmap
  • Les gros chunks ne passent pas par un cache intermédiaire de freelist, ce qui simplifie la procédure d’exploitation
  • Si les métadonnées de taille en tête d’un chunk d’allocation sont falsifiées avec une autre taille de page puis que free est appelé, un munmap peut se produire sur la zone mémoire adjacente au chunk
  • L’exemple de Dulin cible les zones .gnu.hash et .dynsym avec un munmap arbitraire
    • Il les remplit ensuite à nouveau avec un chunk mmap plus grand
    • Cela permet d’écraser une entrée PLT pas encore résolue
    • L’attaque de type GOT overwrite redevient possible
  • Le PoC abrégé alloue deux gros chunks, modifie le champ size de top[-1], puis appelle free(top) pour unmapper aussi les zones adjacentes, avant de remplir à nouveau avec des données X via une allocation plus grande
  • Cette technique peut fonctionner même en présence de CFI et ne nécessite pas de fuite ASLR préalable
  • Le scellement de l’ensemble de VMA prévu dans l’intégration de mseal à glibc devrait automatiquement atténuer cette attaque en protégeant le code binaire mappé et les bibliothèques dynamiques contre les astuces d’unmapping/remapping
  • Les développeurs qui veulent un durcissement supplémentaire peuvent sceller sélectivement les allocations mmap qui ne seront pas étendues ni unmappées pendant toute la durée de vie du programme

Portée d’utilisation à venir

  • mseal est une fonctionnalité d’atténuation relativement récente du noyau Linux, et d’autres cas d’usage encore non abordés peuvent exister
  • Une fois l’intégration dans glibc terminée et stabilisée, des améliorations adaptées aux exigences du syscall pourront suivre
  • Le paramètre flags, actuellement inutilisé, pourrait aussi se voir attribuer des usages précis à l’avenir
  • Lors de son application à des logiciels réels, il faut évaluer à la fois la durée de vie de la mémoire à sceller et ses besoins de modification

1 commentaires

 
GN⁺ 2024-10-27
Commentaires sur Hacker News
  • Ce serait bien qu’un initié résume quelles oppositions et quelles inquiétudes ont émergé lors des discussions musclées sur la mailing list du kernel
    J’évite en général la mailing list elle-même quand ça devient trop épicé, j’ai déjà assez mal à la tête comme ça
    Le mécanisme en lui-même paraît raisonnable, mais c’est surprenant qu’une telle fonctionnalité n’existait pas déjà dans le kernel

    • Je ne sais pas s’il y a eu d’autres fils en dehors de celui lié, mais c’était globalement du franc-parler à la Linus
      Parmi les flags proposés, certains permettaient d’ignorer le seal, et Linus a réagi en mode : « donc à un endroit on empêche munmap, mais ailleurs on ignorerait le seal ? »
      Il a ensuite insisté plus fermement sur le fait qu’une fois scellé, c’est scellé, et qu’on ne peut pas avoir « des endroits qui respectent le seal et d’autres arbitrairement qui ne le respectent pas »
      Plus tard, il a même dit qu’un seal ne pouvait pas être ignoré, que ce n’était pas négociable, et que toute nouvelle proposition de flag pour ignorer le seal finirait sur son ignore list
    • https://lwn.net/ml/linux-kernel/7071.1697661373@cvs.openbsd....
      Dans un mail de Theo de Raadt à Jeff Xu, en réponse à l’idée que les approches mimmutable() et mseal() diffèrent mais visent toutes deux à rendre les applications Linux plus sûres en scellant la mémoire contre les attaquants, il répond qu’« on dirait que vous êtes en train de créer mseal uniquement pour Chrome »
      Selon lui, cela ne conviendrait pas bien aux applications en général, parce que c’est trop complexe et que, d’après l’expérience de mimmutable(), ce n’est pas l’application elle-même qui le gère directement mais plutôt execve(), l’initialisation de libc et ld.so
    • Matthew Wilcox a lui aussi répondu sèchement à Jeff Xu, en gros : « merci de montrer que tu ne sais pas ce qu’il faut empêcher »
      Il était d’accord avec Theo et Linus, estimant que l’idée de base de mimmutable() est bonne, mais que la manière de la découper et de l’implémenter est horrible
    • Cet article peut aider : https://lwn.net/Articles/948129/
  • Je me demande comment utiliser concrètement cet appel système
    Chrome le veut, mais puisqu’un attaquant peut remapper avec d’autres flags, les pages scellées ne peuvent pas être unmappées
    Du coup, j’ai l’impression qu’on ne peut pratiquement pas l’utiliser pour des pages allouées à l’exécution, sauf si on compte les garder pendant toute la durée de vie du processus
    Si c’est le cas, est-ce que ce n’est pas difficile à appliquer à des cibles très attractives comme la mémoire de sandbox JS ?
    Je me demande si ce genre de travail se résout en l’exécutant dans un processus séparé, en scellant la mémoire, puis en tuant le processus à la fin
    Je ne connais pas assez bien la gestion mémoire et processus de Chrome pour comprendre pourquoi ce ne serait pas un problème, et je me demande si c’est aussi pour cela qu’on explique souvent que cette fonctionnalité n’est pas utile à la plupart des programmes

    • Ici, le multi-processus peut être une option
      Autant que je sache, Chrome l’utilise déjà massivement, et comme un processus séparé est de toute façon nécessaire pour des raisons comme l’isolation via les namespaces, c’est peut-être la direction retenue
  • Discussion après mseal(), le 20 octobre 2023 : https://lwn.net/Articles/948129/
    mseal() se rapproche, le 19 janvier 2024 : https://lwn.net/Articles/958438/
    Scellement mémoire pour la GNU C Library, le 12 juin 2024 : https://lwn.net/Articles/978010/

  • C’est dommage que, malgré les nombreuses fonctionnalités des architectures x86_64 modernes qui aident à la programmation et au calcul sûrs, l’OS doive encore implémenter ce genre d’appel
    À mon avis, cette manière de rafistoler de vieux systèmes bâtis sur des héritages et des façons de penser obsolètes, sur des paradigmes qui ne correspondent plus au monde ni aux connaissances actuelles, ralentit les progrès de l’informatique et met littéralement des milliards de personnes en danger
    Je ne dis pas pour autant que ce changement n’est pas un pas dans la bonne direction, mais si l’on renonce à certains idéaux historiques sur la façon dont un OS doit fonctionner et qu’on tient compte des systèmes actuels, de nos connaissances et de ce que les gens attendent d’eux, on peut imaginer des systèmes libérés du poids et des risques imposés aujourd’hui aux développeurs et aux utilisateurs
    Certes, il existe des bugs d’architecture, mais comme les logiciels n’exploitent même pas correctement les fonctionnalités actuelles, invoquer ces bugs d’architecture rate le cœur du sujet
    Si l’on construit sur des fondations instables, il existera toujours des moyens moins coûteux de compromettre le système

    • Je suis curieux : j’aimerais savoir quelles fonctionnalités de sécurité peu ou pas utilisées existent sur x86_64 et peuvent être exploitées sans que l’OS ait besoin de les utiliser ou de les activer
      J’aimerais aussi comprendre pourquoi mseal ne serait pas justifié, et ce qui serait meilleur à la place
  • Peut-on écraser ou désactiver l’appel système mseal avec une astuce de type LD_PRELOAD ?

    • Contrairement aux mécanismes de protection mémoire existants sous Linux, mseal est un appel système conçu non pas contre un attaquant local cherchant à extraire des secrets sensibles en mémoire, mais pour atténuer les attaques par exécution de code à distance
      Si un attaquant distant peut modifier l’environnement local, il faut considérer que le système est déjà compromis
    • Probablement pas avec LD_PRELOAD
      Pour que cela fonctionne, il faut passer par une fonction importée, or un appel système brut ne peut pas être intercepté de cette manière
      Discussion sur l’interception des appels système Linux : https://stackoverflow.com/questions/69859/how-could-i-interc...
      Le plus simple pour « désactiver » la fonctionnalité serait peut-être de créer directement un kernel patché qui fait semblant que mseal fonctionne
      Cela dit, comme un programme qui utilise mseal peut vérifier qu’il fonctionne réellement, un kernel compromis devrait aussi trouver un moyen de désactiver mseal discrètement afin d’empêcher les vérifications de l’application après déploiement
    • Si l’on contrôle le processus très tôt, il existe pas mal de moyens de l’écraser
      Par exemple, on peut suivre l’exécutable avec ptrace, surveiller les appels système, puis faire en sorte de sauter mseal(2)
      Cet appel système n’est pas conçu pour un modèle de menace où « l’attaquant avait déjà un accès avant l’initialisation du processus »
    • On peut écraser le wrapper de l’appel mseal, mais pas l’appel système lui-même
      À ce que j’ai vu, toutes les méthodes d’écrasement d’appels système par préchargement remplacent le wrapper ; si l’appel système est invoqué directement, il semble qu’on ne puisse pas l’écraser
      Techniquement, il serait peut-être possible d’écraser la fonction syscall elle-même
    • Selon https://lwn.net/Articles/978010/, un glibc tunable est prévu
  • Meta : le prototype mseal() figurant dans l’article doit être corrigé
    Le premier argument y est indiqué comme unsigned start addr, mais ce devrait probablement être unsigned long start_addr

    • Cela semble correct maintenant : int mseal(unsigned long start, size_t len, unsigned long flags)
  • La question de la manière dont CPython devrait prendre en charge l’appel système mseal() a déjà été discutée dans « Memory Sealing ‘Mseal’ System Call Merged for Linux 6.10 » (2024) https://news.ycombinator.com/item?id=40474510#40474551

  • OpenBSD dispose d’une fonctionnalité de ce type depuis un moment https://man.openbsd.org/mimmutable.2
    Je me demande pourquoi une fonctionnalité aussi évidente n’arrive que maintenant dans Linux

    • Le mimmutable d’OpenBSD a été introduit dans OpenBSD 7.3, publié le 10 avril 2023, donc ce n’est pas « depuis longtemps »
      En revanche, Linux et FreeBSD avaient memfd_create depuis longtemps, tandis qu’OpenBSD n’a pas de fichiers anonymes et dépend donc de shm_open