1 points par GN⁺ 2024-08-10 | 1 commentaires | Partager sur WhatsApp
  • Google a découvert CVE-2023-2163 dans le verifier eBPF avec Buzzer, et a confirmé que cette vulnérabilité pouvait être exploitée pour une élévation locale de privilèges et une évasion de conteneur
  • eBPF permet d’étendre les fonctions du noyau à l’exécution, mais comme il exécute du bytecode arbitraire avec des privilèges élevés, la vérification par le verifier avant chargement constitue un pilier central de la sécurité
  • Le problème vient du fait que le path pruning du verifier considérait à tort que certains chemins d’exécution différents étaient équivalents à un chemin déjà jugé sûr
  • L’exploit tire parti de l’écart entre un registre vu comme valant 0 par le verifier et sa valeur réelle à l’exécution, afin de corrompre le pointeur de pile eBPF, puis d’obtenir des lectures/écritures arbitraires et un contournement de la KASLR
  • Le correctif consiste à marquer comme precise les registres imprecise qui influencent un registre precise, et la même stratégie de fuzzing sur l’arithmétique des pointeurs n’a ensuite révélé aucun problème supplémentaire

Le verifier eBPF élargit la surface d’attaque du noyau

  • eBPF est une technologie qui permet d’étendre à l’exécution les fonctionnalités du noyau Linux sans avoir recours à des modules noyau complexes
  • Les programmes eBPF sont écrits en bytecode personnalisé et subissent une vérification de sûreté avant d’être exécutés lors de certains événements
    • Un exemple typique est un programme eBPF exécuté lors d’un appel syscall précis
  • Cette architecture permet d’exécuter du code arbitraire à un niveau de privilège élevé, ce qui augmente fortement la surface d’attaque du noyau
  • Avant d’être chargé, un programme doit passer le verifier, qui vérifie que les hypothèses de sécurité d’eBPF sont respectées
  • Lorsqu’une vulnérabilité du verifier est exploitée, elle mène généralement à une élévation locale de privilèges ou à une évasion de conteneur dans un environnement conteneurisé

Buzzer et le fuzzing de l’arithmétique des pointeurs

  • Google a créé Buzzer pour auditer automatiquement le code du verifier eBPF
  • Buzzer est un fuzzer qui génère en masse des programmes eBPF syntaxiquement valides et peut être configuré avec des stratégies destinées à déclencher des bogues logiques
  • La découverte de CVE-2023-2163 a utilisé la stratégie d’arithmétique des pointeurs
    • Elle génère un en-tête qui initialise les registres avec des valeurs arbitraires
    • Elle produit des séquences arbitraires d’instructions arithmétiques et de saut
    • Elle choisit un registre au hasard et lui applique une addition avec un pointeur d’élément de map eBPF
    • Elle écrit ensuite une valeur magique dans cet élément
    • Si la valeur écrite n’est pas observée depuis l’espace utilisateur, il est possible qu’une écriture hors limites se soit produite
  • CVE-2023-2163, découverte par Buzzer, se trouvait dans la logique de path pruning d’eBPF et a débouché sur un exploit utilisable à la fois pour l’évasion de conteneur et l’élévation locale de privilèges

Bogue de path pruning et de suivi precise

  • Le verifier eBPF simule les chemins d’exécution possibles pour vérifier qu’un programme peut s’exécuter en toute sécurité
  • Lors d’un branchement conditionnel, si la valeur d’un registre n’est pas certaine, le verifier tente de suivre l’exécution pour tous les états possibles
  • Plus il y a de sauts conditionnels, plus le nombre de chemins d’exécution croît de façon exponentielle, ce qui nuit aussi aux performances lors du chargement du programme dans le noyau
  • Pour réduire cela, les développeurs eBPF ont introduit le path pruning
    • Si le verifier peut garantir qu’un état équivalent à l’état courant a déjà atteint en toute sécurité une instruction exit, il n’explore pas davantage ce chemin
  • Pour rendre ce pruning plus efficace, le concept de precise tracking est également utilisé
    • Un registre est marqué precise s’il participe à une opération d’arithmétique de pointeur ou s’il est passé comme constante à une fonction helper
    • Le verifier doit alors explorer tous les états impliquant ce registre
  • Dans CVE-2023-2163, r9, qui influençait la précision de r6, n’était pas correctement marqué
    • Le verifier supposait que r9 ne contribuait pas au caractère precise de r6
    • Après avoir déterminé qu’un chemin précédent permettait d’atteindre exit en toute sécurité avec r6, il a considéré les autres états comme équivalents et les a élagués
    • À l’exécution, le chemin 1:2:4:6 était suivi, et au point 6, r6, que le verifier croyait égal à 0, pouvait en réalité avoir une autre valeur et être utilisé dans une opération d’arithmétique de pointeur

Lecture/écriture arbitraire et contournement de la KASLR

  • Le code de l’exploit est publié dans le dépôt de recherche en sécurité de Google
  • Le développement de l’exploit s’est fortement appuyé sur les travaux de @chompie et @_manfp
  • Le déroulement général consiste d’abord à obtenir une lecture/écriture arbitraire, puis à localiser les credentials du processus pour modifier l’uid et le pointeur fs_struct afin d’élever les privilèges
  • Lors de la première étape, la valeur de registre corrompue est transformée en 1
    • Une analyse manuelle a montré que la valeur de r6 à l’exécution était 0x400, alors que le verifier la voyait comme 0
    • L’instruction r6 >>= 10 a permis d’obtenir la valeur voulue, 1
  • Ensuite, la fonction helper bpf_skb_load_bytes_relative est utilisée pour corrompre un pointeur de la pile eBPF
    • Le verifier estime que len vaut 8, mais à l’exécution, à cause de la valeur de r6, len vaut en réalité 9
    • En conséquence, 9 octets sont écrits au lieu de 8, ce qui corrompt le premier octet de la valeur à l’offset de pile -32
  • En manipulant le pointeur de pile corrompu, il devient possible de divulguer un pointeur de map eBPF
    • Depuis l’espace utilisateur, les valeurs de R2 et R3 sont lues, puis on vérifie si R2 vaut 0xBACA
    • Si cette condition est remplie, R3 devient la valeur divulguée du pointeur de map
    • La fuite du pointeur de map eBPF permet alors de contourner la KASLR
  • Avec la même stratégie, si au lieu d’un seul octet on écrase toute une zone continue de la pile avec le pointeur souhaité, une lecture/écriture arbitraire devient possible
    • Du point de vue du verifier, il s’agit d’une manipulation de pile BPF, mais en réalité cela permet de lire et d’écrire la mémoire du noyau

De la fuite de map au shell root

  • Après la fuite du pointeur de map, l’exploit ne diffère pas beaucoup de celui de Chompie et en reprend une partie du code
  • La procédure générale est la suivante
    • Recherche répétée de la chaîne init_pid_ns dans kstrtab
    • Recherche du symbole ksymtab qui référence cette chaîne afin d’obtenir l’adresse de la structure init_pid_ns
    • Parcours du radix tree pour trouver l’entrée dont le champ comm correspond au nom du binaire de l’exploit en cours d’exécution
      • Lorsqu’il s’exécute dans un conteneur, le PID n’est pas un heuristique fiable, donc l’exploit ne s’appuie pas uniquement sur lui
    • Modification de l’uid à 0 et du pointeur fs_struct
    • Si fs_struct est remplacé par la même valeur que le pointeur référencé par le PID 1, il devient possible d’observer le système de fichiers de l’hôte depuis le conteneur
    • Exécution de system("/bin/bash") pour obtenir un shell root
  • Le code publié sur GitHub ne permet l’évasion de conteneur que sur certaines versions de Linux
  • Sur Ubuntu et certaines distributions, les offsets de la structure de données écrasée diffèrent, ce qui limite l’exploit à une simple élévation locale de privilèges
  • Des ajustements du code sont nécessaires pour qu’il fonctionne sur toutes les distributions Linux

Correctif et validation complémentaire

  • L’analyse de la cause de CVE-2023-2163 et le correctif peuvent être consultés sur la kernel mailing list
  • La correction consiste à marquer comme precise les registres imprecise impliqués dans les opérations qui affectent un registre precise
  • Il n’est pas clairement établi si ce correctif affecte les performances du verifier eBPF
  • La même stratégie de fuzzing de l’arithmétique des pointeurs a continué d’être exécutée, mais aucun problème supplémentaire n’a été découvert
  • Buzzer est toujours en cours de développement, et le projet reçoit les contributions de la communauté open source via son repo GitHub

1 commentaires

 
GN⁺ 2024-08-10
Avis sur Hacker News
  • Sur les plateformes où eBPF est le plus couramment utilisé, le code non privilégié ne peut de toute façon pas charger de programmes eBPF, si bien que l’impact des bugs du vérificateur est souvent limité.
    Au final, ce type de bug est une vulnérabilité root → ring0 ; cela ne veut pas dire que ce n’est rien, mais côté serveur, c’est généralement un compromis acceptable.
    En particulier, l’historique d’élévations de privilèges locales dans le kernel via eBPF est plutôt bon comparé à l’ensemble du kernel, et dans l’environnement eBPF actuel, la principale valeur du vérificateur est d’empêcher qu’un mauvais programme eBPF tue accidentellement le kernel.
    C’est une affirmation qui ne tient absolument pas, au point d’en être risible, pour les modules kernel chargeables classiques.

    • Le PoC écrit dans une map eBPF via un pointeur hors limites, mais s’il s’agit d’une simple erreur de suivi de plage de valeurs scalaires, elle semble aussi exploitable avec des programmes BPF non étendus chargeables via seccomp.
      Dans ce cas, aucun privilège n’est requis sur la plupart des plateformes.
      Et avec les namespaces utilisateur non privilégiés, on peut devenir soi-même « root », donc le passage root → ring0 devient aussi moins restrictif.
      C’est une tendance qu’on a vue régulièrement dans les PoC de bugs eBPF depuis que les distributions l’ont activé puis, pour la plupart, désactivé à nouveau.
    • Il ne faut pas non plus oublier qu’on peut donner CAP_BPF à un conteneur.
      À mesure que des outils comme Cilium prennent de l’ampleur, les chemins d’attaque menant à un environnement de conteneur doté de cap_bpf deviennent de plus en plus réalistes.
    • Corriger les bugs du vérificateur est important, car c’est une condition préalable pour rendre sûre l’utilisation non privilégiée d’eBPF.
  • « Uno no es ninguno » signifie littéralement « un n’est pas aucun », soit quelque chose de proche de « One is not none ».

    • En espagnol, les doubles négations ne sont souvent pas de véritables doubles négations.
      Par exemple, « il n’y a rien ici » se dit « no hay nada aquí », ce qui, mot à mot, semble vouloir dire « il n’est pas vrai qu’il n’y a rien ici ».
      La Royal Spanish Academy l’explique également ainsi :

      https://www.rae.es/espanol-al-dia/doble-negacion-no-vino-nad...

      La soi-disant « double négation » vient de la concordance négative qui doit obligatoirement s’appliquer dans certaines situations en espagnol et dans d’autres langues romanes ; en conséquence, l’adverbe no et d’autres éléments à valeur négative apparaissent ensemble dans une même phrase.
      La présence simultanée de ces deux « négations » n’annule pas le sens négatif de la phrase.

  • Quand j’ai essayé d’utiliser eBPF par le passé, il n’était pas assez expressif pour ce que je devais faire.
    Je me demande vraiment s’il est justifié d’augmenter la complexité de l’espace kernel pour obtenir cette flexibilité limitée.
    Pour le filtrage de paquets, je comprends, mais son usage pour d’autres finalités comme le sandboxing me paraît moins convaincant.

    • Il existe aussi d’autres technologies pour ce genre d’usage, comme DTrace.
      Le choix pour le kernel n’est pas eBPF ou rien, mais eBPF ou quelque chose d’autre du même genre.
      On peut ne pas l’utiliser beaucoup soi-même, mais il y a des gens qui s’en servent toute la journée.
      Il me semble que des ingénieurs des FAANG disaient faire tourner en permanence des dizaines, voire peut-être des centaines, de ces programmes sur tous leurs serveurs, sans même compter les usages ponctuels.
      Les FAANG emploient aussi des développeurs kernel dédiés, donc elles financent en quelque sorte cette complexité dont elles se servent.
      J’ai moi aussi résolu des problèmes avec eBPF.
      Sans eBPF, ces problèmes auraient été pratiquement insolubles pour quelqu’un qui n’est pas expert du kernel ; on n’en a pas besoin souvent, mais quand c’est le cas, il n’y a pas de substitut.
      Dans certains cas, même pour un expert du kernel, le choix se résume à utiliser eBPF ou à maintenir éternellement un patch kernel personnalisé.
    • Les pilotes kernel chargeables traditionnels ne seraient-ils pas préférables à un patch ou à eBPF ?
      Je sais que ce n’est pas sûr, mais les gens qui les manipulent savent qu’un grand pouvoir implique de grandes responsabilités.
  • Il me semble que « Uno no es ninguno » devrait bien se traduire par « One is not none ».

    https://bughunters.google.com/blog/6303226026131456/a-deep-d...

    • Sinon, c’est traduit par « One is none ».
      C’est la fameuse double négation qui fait souffrir les locuteurs étrangers, moi y compris.

      https://spanish.stackexchange.com/questions/26777/how-does-d...

    • Littéralement, oui, mais en espagnol, bizarrement, la double négation sert généralement de simple négation.

    • « one ain't nothin' » serait peut-être une meilleure manière de le rendre.

  • Dans notre pays, on a une expression : « un hérisson dans le pantalon ».
    Aussi utile que cela puisse être, ça ne donne pas l’impression d’avoir été écrit de façon sûre et prudente.

    • Avec l’expérience, on finit par comprendre que même en procédant de façon sûre et prudente, des erreurs peuvent arriver.