Analyse approfondie du nouveau syscall mseal de Linux
(blog.trailofbits.com)- 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)ajouteVM_SEALEDà une plage de VMA alignée sur les pages, et le noyau refuse les changements viamprotect,munmap,mmap(MAP_FIXED),mremapet certains chemins destructeurs demadvise- Alors que
memfd_createoumemfd_secretrelè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 viamunmap/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_createetmemfd_secretrelèvent plutôt du scellement de fichiers (file sealing)- Ils permettent de créer des fichiers anonymes basés en RAM
memfd_secretfait 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
msealest 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)startetlenindiquent le début et la longueur de la VMA valide à scellerlendoit être correctement aligné sur les pages- Pour l’instant,
flagsn’est pas utilisé et doit valoir0
- L’implémentation dans Linux 6.12 appelle
do_msealcurrent->mmpointe vers lamm_struct, c’est-à-dire tout l’espace d’adressage mémoire virtuel du processus appelant- Le champ
mmapdemm_structcontient la liste desvm_area_struct, les zones de mémoire contiguës créées parmmap - Des zones comme la pile ou le VDSO peuvent aussi être représentées comme une VMA
do_msealcalcule l’adresse de fin de la plage, puis verrouille la zone mémoire avecmmap_write_lock_killablecheck_mm_sealparcourt 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
- Si de la mémoire non allouée se trouve au milieu de la plage, il renvoie
apply_mm_sealparcourt à nouveau la même plage et ajoute le flagVM_SEALEDaux VMA cibles viamseal_fixup
Opérations mémoire interdites après scellement
- Le patchset du noyau ajoute des vérifications de
VM_SEALEDdansmm/madvise.c,mm/mmap.c,mm/mprotect.c,mm/mremap.cetmm/mseal.c mprotectetpkey_mprotectappellent en internecan_modify_vmadepuismprotect_fixup, et renvoient-EPERMsi 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
mprotectoupkey_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) madviseavec certains flags destructeurs
- Changement des bits de permission via
- 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 demunmap - À partir de Linux 6.10,
msealpeut être utilisé par appel direct au syscall ; l’exemple de wrapper utilise le numéro de syscall462etflags=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érificationcan_modify_vmaempê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,mprotectne 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
msealsera 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
brkpour récupérer de l’espace - Sur ce chemin, une réduction peut se produire via
arch_unmapetdo_vmi_unmap - En état scellé, ce type d’unmapping n’est pas autorisé, ce qui peut casser l’allocation dynamique de mémoire
- L’allocateur du tas peut appeler le syscall
- 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
msealsur 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
mmapet 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
mprotectempê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
msealpeut 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,mallocetfreeappellent respectivement directementmmapetmunmap - 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
freeest appelé, unmunmappeut se produire sur la zone mémoire adjacente au chunk - L’exemple de Dulin cible les zones
.gnu.hashet.dynsymavec unmunmaparbitraire- Il les remplit ensuite à nouveau avec un chunk
mmapplus grand - Cela permet d’écraser une entrée PLT pas encore résolue
- L’attaque de type GOT overwrite redevient possible
- Il les remplit ensuite à nouveau avec un chunk
- Le PoC abrégé alloue deux gros chunks, modifie le champ size de
top[-1], puis appellefree(top)pour unmapper aussi les zones adjacentes, avant de remplir à nouveau avec des donnéesXvia 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
mmapqui ne seront pas étendues ni unmappées pendant toute la durée de vie du programme
Portée d’utilisation à venir
msealest 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
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
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
Dans un mail de Theo de Raadt à Jeff Xu, en réponse à l’idée que les approches
mimmutable()etmseal()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ôtexecve(), l’initialisation de libc etld.soIl é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 horribleJe 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
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
J’aimerais aussi comprendre pourquoi
msealne serait pas justifié, et ce qui serait meilleur à la placePeut-on écraser ou désactiver l’appel système
msealavec une astuce de typeLD_PRELOAD?msealest 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 à distanceSi un attaquant distant peut modifier l’environnement local, il faut considérer que le système est déjà compromis
LD_PRELOADPour 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
msealfonctionneCela dit, comme un programme qui utilise
msealpeut vérifier qu’il fonctionne réellement, un kernel compromis devrait aussi trouver un moyen de désactivermsealdiscrètement afin d’empêcher les vérifications de l’application après déploiementPar exemple, on peut suivre l’exécutable avec
ptrace, surveiller les appels système, puis faire en sorte de sautermseal(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 »
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
syscallelle-mêmeMeta : le prototype
mseal()figurant dans l’article doit être corrigéLe premier argument y est indiqué comme
unsigned start addr, mais ce devrait probablement êtreunsigned long start_addrint 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#40474551OpenBSD 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
mimmutabled’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_createdepuis longtemps, tandis qu’OpenBSD n’a pas de fichiers anonymes et dépend donc deshm_open