5 points par GN⁺ 2024-09-27 | 1 commentaires | Partager sur WhatsApp

Éliminer à la source les vulnérabilités de sûreté mémoire

Un résultat paradoxal

  • Quand une base de code écrite dans des langages non sûrs en mémoire continue de croître, le fait de basculer les nouvelles fonctionnalités vers des langages sûrs en mémoire réduit fortement les vulnérabilités de sûreté mémoire
  • Cela s’explique par le fait que les vulnérabilités diminuent de manière exponentielle avec le temps

Explication mathématique

  • La durée de vie des vulnérabilités suit une distribution exponentielle
  • Les vulnérabilités apparaissent principalement dans le nouveau code, puis le code devient plus sûr avec le temps
  • La densité de vulnérabilités dans un code âgé de 5 ans est de 3,4 à 7,4 fois plus faible que dans un nouveau code

Cas concret sur Android

  • Depuis 2019, l’équipe Android a commencé à faire évoluer les nouveaux développements vers des langages sûrs en mémoire
  • En 2024, la part des vulnérabilités de sûreté mémoire est passée de 76 % à 24 %
  • À mesure que les vulnérabilités de sûreté mémoire diminuent, le risque de sécurité global baisse également

Évolution de la stratégie de sûreté mémoire

  • 1re génération : correctifs réactifs — découvrir les vulnérabilités et les corriger
  • 2e génération : atténuation proactive — rendre l’exploitation des vulnérabilités plus difficile
  • 3e génération : détection proactive des vulnérabilités — les identifier à l’avance
  • 4e génération : prévention à haute confiance — prévenir l’apparition même des vulnérabilités en migrant vers des langages sûrs en mémoire

Les avantages de la prévention à haute confiance

  • Met fin à la compétition sans fin entre défenseurs et attaquants
  • Renforce la sécurité tout en réduisant les coûts grâce aux langages sûrs en mémoire
  • Améliore la justesse du code et la productivité des développeurs

Des enseignements à la pratique

  • Il n’est pas nécessaire d’abandonner ou de réécrire tout le code existant non sûr en mémoire
  • L’amélioration de l’interopérabilité accélère la transition vers des langages sûrs en mémoire
  • Développement d’outils pour améliorer l’interopérabilité entre Rust et C++, ainsi qu’entre Rust et Kotlin

Le rôle des générations précédentes

  • Utilisation sélective de l’atténuation proactive et de la détection
  • À mesure que la transition vers du code sûr en mémoire progresse, le besoin d’atténuation et de détection diminue

Conclusion

  • L’utilisation de langages sûrs en mémoire pour le nouveau code entraîne une baisse exponentielle des vulnérabilités
  • Plus de 6 ans de résultats cohérents sur Android prouvent l’efficacité de cette approche

Le résumé de GN⁺

  • Il est important de migrer vers des langages sûrs en mémoire pour réduire les vulnérabilités de sûreté mémoire
  • Le cas de l’équipe Android montre une forte diminution des vulnérabilités de sûreté mémoire
  • Améliorer l’interopérabilité est plus pragmatique qu’une réécriture complète du code existant
  • Utiliser des langages sûrs en mémoire comme Rust peut améliorer à la fois la sécurité et la productivité

1 commentaires

 
GN⁺ 2024-09-27
Avis sur Hacker News
  • Article intéressant. L’idée centrale est qu’il n’est pas nécessaire de tout réécrire dans le monde entier.
    Le simple fait de déplacer les nouveaux développements vers des langages à sûreté mémoire permet déjà une amélioration significative, et c’est beaucoup plus simple et moins coûteux que de tout porter pour en voir les effets.

    • Ce résultat suggère que, du point de vue de la sécurité, les bénéfices d’une réécriture complète sont limités. Une stratégie consistant à conserver le code legacy mature et à n’utiliser des langages à sûreté mémoire que pour le nouveau code devient plus rentable.
      La valeur des langages et outils capables de s’intégrer solidement avec du code legacy non sûr augmente également.
    • Les réécritures sont tellement coûteuses qu’elles ne peuvent être que très rares, mais l’approche consistant à grignoter progressivement par les bords est très efficace.
      Je me demande aussi s’il n’existe pas une sorte d’effet de « courbe en baignoire » pour le code très ancien. Je me souviens qu’au moment de graves vulnérabilités d’OpenSSL, probablement Heartbleed, beaucoup de gens avaient regardé le code et avaient été horrifiés.
    • Du point de vue de la sécurité, je suis d’accord, mais si l’on veut éliminer le garbage collection ou réduire l’utilisation globale des ressources, l’histoire peut être différente.
    • Le fait que l’écriture de tout le nouveau code dans des langages à sûreté mémoire réduise les vulnérabilités de sûreté mémoire n’a rien de surprenant en soi. La probabilité que du nouveau code crée de nouveaux problèmes de sûreté mémoire est plus élevée que celle de découvrir des problèmes existants dans l’ancien code.
      Cela dit, il est impressionnant que, de 2019 à 2023, sans réécriture, le simple ajout de nouveau code dans des langages à sûreté mémoire, sans doute surtout Rust et un peu Kotlin, ait fait baisser les problèmes de près de 60 %. Je me demande aussi pourquoi il y a eu une phase de stagnation entre 2021 et 2023.
      L’extrapolation des chiffres de 2024 laisse voir une volonté de montrer un bon chiffre ; il aurait été plus utile d’analyser et d’expliquer pourquoi il y a eu une hausse entre 2022 et 2023.
      Il manque aussi des informations sur les langages utilisés pour ce nouveau code à sûreté mémoire, et dans quelles proportions. Ils semblent utiliser à la fois Rust et Kotlin, mais je me demande si Rust représente 95 % ou 50 %, et, si Kotlin pèse lourd, dans quels domaines il est utilisé à la place de Rust.
  • Les graphiques de cet article se distinguent par leur clarté et leur concision. Ils montrent bien qu’un choix soigneux des données et de leur étiquetage peut intégrer naturellement, dans le texte, l’idée que l’on veut faire passer.
    La conclusion du fait que les vulnérabilités diminuent exponentiellement est qu’il faut se concentrer sur le code purement nouveau. Consacrer des ressources à un gigantesque projet indiscriminé de « réécriture en Rust » est inefficace, même au regard de l’objectif de maximiser la sûreté mémoire.
    Il est assez satisfaisant que la stratégie la plus simple, et celle recommandée par les praticiens pragmatiques de Rust, soit aussi, d’après les données, la meilleure stratégie pour minimiser les vulnérabilités mémoire.
    Le passage selon lequel « l’équipe Android a observé que le taux de rollback des changements Rust était inférieur à la moitié de celui du C++ » est surprenant.

  • « Les vulnérabilités diminuent exponentiellement. Elles ont une demi-vie. [...] Une vaste étude sur la durée de vie des vulnérabilités, présentée à Usenix Security en 2022, a également confirmé ce phénomène. Les chercheurs ont constaté que la grande majorité des vulnérabilités se trouvent dans du code nouveau ou récemment modifié. »
    Dans ce cas, il pourrait être préférable, pour la sécurité, de cesser d’ajouter de nouvelles fonctionnalités qui ne sont pas strictement nécessaires. Windows LTSC est probablement la version la plus sûre de Windows.

    • Dans les logiciels maintenus, les bugs individuels qui surviennent pendant un usage normal diminuent effectivement avec le temps. Quand un bug cause un problème, quelqu’un le signale, et une partie de ces bugs est corrigée. C’est le mécanisme de diminution.
      Mais pour les vulnérabilités qui n’ont pas encore été exploitées, il n’existe pas un tel mécanisme de diminution. Elles restent simplement là sans susciter de plaintes d’utilisateurs ni de rapports de bugs, jusqu’à ce qu’un adversaire disposant de ressources et de motivations suffisantes les découvre et les exploite.
      Il y a aujourd’hui davantage d’adversaires de ce niveau qu’auparavant.
    • L’affirmation « les vulnérabilités diminuent exponentiellement » s’appliquerait probablement non seulement aux vulnérabilités, mais aussi à tous les types de bugs. J’ai observé ce phénomène dans les tests de SBCL, un projet de logiciel libre auquel je participe. Les nouveaux bugs ont fortement tendance à apparaître dans les parties récemment modifiées.
      Bien sûr, cela ne veut pas dire que tous les bugs se trouvent uniquement dans du code récent. On a tous vu des bugs restés non détectés pendant des années. Pour ces bugs-là, il faut se demander pourquoi les tests les ont manqués.
      Les tests devraient donc se concentrer sur le code récemment modifié. En particulier, le test par mutation peut être appliqué de façon très ciblée au code modifié ou au code fortement couplé à celui-ci, ce qui peut réduire fortement la charge de ce type de test.
      Google disposait d’un système qui utilisait les tests par mutation de cette manière en complément de la revue de code.
    • Même si de nouvelles fonctionnalités ne sont pas indispensables pour vendre un logiciel, le nouveau matériel, de meilleurs algorithmes de sécurité et l’abandon complet des anciens algorithmes continuent d’arriver. Au bout du compte, du nouveau code sera introduit.
    • Une autre approche est possible : ne compiler explicitement que le sous-ensemble des fonctionnalités nécessaires.
      Bien sûr, son caractère pratique varie beaucoup selon les contextes, mais cette méthode devrait être plus courante qu’elle ne l’est aujourd’hui.
    • C’est pourquoi je conseille à la plupart des gens de ne pas adopter immédiatement la dernière version mineure d’un langage ou d’une bibliothèque.
      Les versions à la pointe contiennent beaucoup de nouvelles vulnérabilités. En général, la plus ancienne version encore prise en charge est souvent la plus sûre.
      Bien sûr, il y a des exceptions lorsque les fonctionnalités d’une nouvelle version apportent de la valeur, mais, dans l’ensemble, je pense qu’il vaut mieux éviter les versions qui se terminent par « .0 ».
  • Il existe une corrélation entre le nouveau code et les vulnérabilités mémoire. Le billet de blog propose aussi une explication possible : les vulnérabilités auraient une demi-vie qui décroît rapidement. Mais je ne comprends pas pourquoi il présente les deux éléments comme ayant une relation de causalité
    Plusieurs explications raisonnables sont possibles pour cette corrélation. Le nouveau code est souvent lié à de nouvelles fonctionnalités, et les gens concentrent leurs efforts sur la recherche de vulnérabilités dans ces nouvelles fonctionnalités. De plus, l’ancien code a davantage été utilisé en conditions réelles et a donc pu exécuter plus souvent les cas limites où se cachent des vulnérabilités mémoire
    Je ne suis pas à l’aise avec l’idée de dire que le nouveau code provoque des vulnérabilités mémoire et que celles-ci ont une demi-vie qui décroît rapidement. En nombre brut, c’est peut-être vrai, mais si l’on pense à des vulnérabilités open source à fort impact comme Heartbleed, ou à des bugs d’invalidation de cache CPU, cela ne semble pas juste du point de vue de l’impact

    • On peut aussi supposer que le code le plus ancien a été écrit par les meilleures personnes, avec moins de pression temporelle
      Mon entreprise actuelle en est aussi un exemple. Le code initial a été écrit par les fondateurs, et une partie du nouveau code est écrite par des contractuels soumis à des délais serrés
    • Cet article prend des données intéressantes issues d’un projet et d’un langage très particuliers et atypiques, dans une culture elle aussi très particulière et atypique, puis les généralise fortement à tout le code avec des chiffres catégoriques
      Si la conclusion selon laquelle « par exemple, en se basant sur la durée de vie moyenne des vulnérabilités, un code vieux de 5 ans a une densité de vulnérabilités 3,4 fois plus faible que du nouveau code, et 7,4 fois plus faible si l’on utilise les durées de vie observées dans Android et Chromium » est correcte, cela veut-il dire qu’on peut écrire du code C bourré de défauts, le laisser vieillir hors ligne pendant 5 ans, et il deviendra sûr ?
      Cette étude contient des données importantes, et il y a probablement une part de vérité sous ce qui est partagé, mais la confiance insuffisamment étayée et l’extension excessive du propos me dérangent beaucoup
  • « Gain de productivité : le codage sûr décale davantage la découverte des bugs vers la gauche, avant le check-in du code, ce qui améliore l’exactitude du code et la productivité des développeurs. Ce changement se voit dans des indicateurs comme le taux de rollback, par exemple les retours en urgence de code dus à des bugs inattendus »
    « L’équipe Android a observé que le taux de rollback des changements en Rust est inférieur à la moitié de celui du C++ »
    Cela fait 20 ans que j’écris du code de production à grande échelle dans plusieurs langages, mais quand j’ai découvert Rust en 2016, j’ai su que c’était le langage dans lequel j’allais m’investir. J’ai aussi acheté le livre de Klabnik et Carol le jour même, et j’ai encore l’exemplaire papier
    Honnêtement, cela m’a redonné de l’amour pour la programmation

    • Je comprends, dans la mesure où la principale raison pour laquelle j’ai dû rollbacker mes commits C++ était un crash causé par un oubli stupide de vérification de pointeur nul. Si Rust empêche ce problème et d’autres erreurs de codage stupides du même genre, alors toute une catégorie de rollbacks disparaît forcément
  • L’article parle de « langages à sûreté mémoire (MSL) » au pluriel, mais le seul langage explicitement cité comme cible de migration et dont l’interopérabilité est en cours d’amélioration est Rust
    Kotlin est aussi mentionné dans le contexte de l’amélioration de l’interopérabilité Rust<>Kotlin, et il dispose dans une certaine mesure de mécanismes de sûreté mémoire, mais je ne sais pas s’il est au même niveau que Rust. Je me demande si Google n’utilise que ces deux-là, ou s’il fait aussi référence à d’autres langages

    • Les personnes qui s’intéressent à ce problème, surtout ces dernières années, ont tendance à préférer l’opposition entre « langages à sûreté mémoire » et « langages non sûrs pour la mémoire »
      Fondamentalement, le fait qu’un langage soit sûr par défaut ou non est à la racine du problème. L’expression vise à se concentrer sur la cause fondamentale plutôt qu’à désigner ou recommander un langage précis
      Dans le cas d’Android, dont parle cet article, je ne connais pas vraiment de tentative de migration vers des langages à sûreté mémoire autres que ces deux-là. Je ne suis pas tout le développement Android en détail, mais je lis pas mal ce genre d’articles, et je ne me souviens pas qu’ils aient parlé d’autre chose que Rust ou Kotlin
    • Ce n’est pas uniquement une histoire de Rust. Réécrire en Java, Python ou Go des services réseau écrits en C est aussi un exemple de migration vers un langage à sûreté mémoire
      L’essentiel est de ne pas exposer son propre code à des bugs de sûreté mémoire. Si ce n’est pas strictement nécessaire, il peut même être préférable de choisir un langage qui n’a pas la gestion manuelle de la mémoire à la Rust
    • Android a abordé plus en détail les langages à sûreté mémoire qu’il utilise dans un précédent billet de blog : https://security.googleblog.com/2022/12/memory-safe-language...
      Google a aussi présenté son point de vue sur la sûreté mémoire dans https://security.googleblog.com/2024/03/secure-by-design-goo..., en couvrant aussi les langages à sûreté mémoire qu’il utilise, comme Java, Go et Rust
    • Il existe de nombreux langages à sûreté mémoire, et Google utilise aussi plusieurs langages comme Rust, Python, Java ou Go. Cela dit, le code bas niveau d’Android était historiquement en C++, et dans ce domaine le principal remplaçant à sûreté mémoire pour les nouveaux développements est Rust
    • Java et Kotlin sont utilisés pour les apps, Rust pour les nouveaux logiciels système
      À l’échelle de Google, Go est utilisé pour certains logiciels système, mais je ne l’ai jamais vu utilisé dans Android
  • Si la durée de vie des vulnérabilités suit une loi exponentielle, alors se concentrer sur des valeurs par défaut sûres, comme la sûreté mémoire dans le nouveau code, est disproportionnellement précieux, à la fois en théorie et d’après six ans de données de la base de code Android
    Étonnant. C’est la première fois que je vois ce type d’argument utilisé pour justifier des garde-fous de sécurité qui déplacent la sécurité vers la gauche, et c’est excellent. C’est particulièrement utile pour de grandes bases de code legacy où l’on pourrait dire : « à quoi bon, on ne bénéficiera pas de la sûreté mémoire dans 100 millions de lignes de C++ legacy »
    Cela semble aussi signifier qu’une détection légère des vulnérabilités peut apporter des gains disproportionnellement importants. Même si l’on ne regarde que le nouveau code et les nouvelles dépendances, pas le backlog

  • La conclusion qu’on en tire ici est un peu dérangeante. L’objection évidente — à savoir qu’on découvre moins de vulnérabilités parce qu’on examine moins attentivement le vieux code — n’est pas traitée.
    Il est bien plus courant de regarder les journaux de commits récents que d’examiner une bibliothèque qui n’a pas changé depuis 20 ans.

    • On n’examinait pas non plus le vieux code avec autant d’attention auparavant, et je ne pense pas que cela ait changé.
      L’article ne propose pas de théorie expliquant pourquoi le vieux code contient moins de bugs, mais mon explication est simple : ils ont déjà été découverts.
      Si l’on suppose que n’importe quel code contient un nombre fixe de bugs inconnus pour 1 000 lignes, alors, avec le temps, le nombre d’exécutions en production avec des entrées variées s’accumule, ce qui augmente les chances de les découvrir. Avec les corrections et les revues de code qui les accompagnent, on peut espérer une amélioration en moyenne.
      Ainsi, avec le temps, le nombre de bugs pour 1 000 lignes de code existant diminue. Il a en quelque sorte été éprouvé sur le terrain.
      Comme le dit l’article, si l’on continue d’introduire de nouveaux bugs au même rythme, il n’y a pas de progrès ; mais si les langages à sûreté mémoire permettent d’introduire moins de bugs dans les nouvelles fonctionnalités, alors le nombre total de bugs diminuera avec le temps.
    • Je ne comprends pas bien cet argument. Le périmètre examiné est Android, et les gens recherchent des vulnérabilités manuellement et automatiquement à partir du code source et des binaires, pas des journaux de commits. Je ne vois pas en quoi les journaux de commits sont pertinents pour trouver des bugs.
      Les commits ne servent qu’à l’attribution. Si une vieille bibliothèque qui n’a pas été modifiée depuis 20 ans a passé 20 ans de fuzzing et de revue manuelle du code, elle a de bonnes chances d’être assez robuste.
    • Je ne suis pas entièrement convaincu par l’affirmation selon laquelle le vieux code contient moins de vulnérabilités. D’autres facteurs que l’âge pourraient expliquer la différence.
      Par exemple, ces dernières années, les ingénieurs se sont peut-être concentrés sur la réécriture des parties les plus risquées dans des langages à sûreté mémoire, tout en modifiant moins le vieux code moins risqué.
      Ou bien des changements de processus ou d’équipe ont pu entraîner une hausse des défauts.
      Cela dit, l’explication selon laquelle chaque bug a une certaine probabilité d’être découvert par unité de temps, et que les défauts restants diminuent avec le temps, est plausible. Si les mainteneurs corrigent plus de vulnérabilités qu’ils n’en introduisent de nouvelles, alors le vieux code contient probablement moins de vulnérabilités, et celles qui restent sont vraisemblablement plus difficiles à trouver.
    • Ce qui m’intéresse, ce ne sont pas les vulnérabilités théoriques, mais celles qui sont réellement exploitées. Si les attaquants ne cherchent pas de vulnérabilités dans un code donné, elles sont pratiquement inexistantes.
  • Je me demande comment ce raisonnement s’applique à Mac et Windows, sachant que le nouveau code Mac est majoritairement écrit en Swift, qui est à sûreté mémoire, tandis que Windows utilise encore surtout C ou C++.

  • Si l’on imagine ici la phase finale, plus les vulnérabilités deviennent rares, plus elles prennent de valeur. Les vulnérabilités restantes seront soigneusement stockées par des acteurs étatiques et réservées à des cibles de grande valeur.
    Si ce billet décrit la quatrième génération, la cinquième pourrait ressembler au Lockdown Mode d’iOS : permettre aux utilisateurs soucieux de sécurité d’activer une case qui renforce la sécurité au prix d’une baisse de performances.
    La case idéale détecterait et capturerait les attaques, par exemple via la virtualisation, puis les enverrait à une équipe sécurité pour analyse. Cela créerait un effet dissuasif pour les attaquants. Ils ne voudront pas brûler une vulnérabilité rare si l’utilisateur a peut-être activé cette case de sécurité, et beaucoup de cibles de grande valeur l’activeront.
    C’est une forme d’immunité collective appliquée aux vulnérabilités logicielles plutôt qu’aux agents pathogènes biologiques.
    Les utilisateurs très sensibles à la sécurité sont probablement aussi très sensibles à la vie privée. Donc, au lieu de transmettre discrètement toute l’activité de l’utilisateur, il faudrait afficher une notification lorsqu’une attaque est détectée. Quelques Ko d’activité réseau anormale pourraient suffire à l’équipe sécurité pour reconstituer l’attaque, avec l’accord de l’utilisateur avant le partage.