- La porte dérobée
xz, découverte en mars 2024, aurait pu conduire à une exécution de code à distance dans les environnementssshdaffectés, et le cas de NixOS montre que les builds reproductibles peuvent révéler mécaniquement une altération de la chaîne d’approvisionnement - L’élément malveillant ne se cachait pas dans l’ensemble du dépôt Git de
xz, mais dans les tarballs de release fournis par le mainteneur pour5.6.0et5.6.1, générant pendant le build un script shell et un fichier objet à partir de faux fichiers de test.xz - L’attaque exploitait les ifunc de
glibcpour exécuter du code lors du chargement deliblzma, et visait les environnements de type Debian et Fedora oùsshddépend transitivement deliblzmavialibsystemd - NixOS privilégie généralement les archives de sources générées automatiquement par GitHub, mais comme
xzintervient dans l’étape de bootstrap denixpkgs, il fallait faire confiance au tarball du mainteneur afin d’éviter un cycle de dépendance avecautoconf - La défense proposée consiste à rebuilder
xzaprès le bootstrap à partir des sources GitHub, puis à comparer le résultat avec l’artefact produit depuis le tarball existant ; une fois la condition activée, une différence de taille deliblzma.soet l’ajout du symbole_get_cpuidsont apparus
Comment la porte dérobée xz s’est introduite
xzest un logiciel de compression et décompression souvent utilisé dans le chemin critique des distributions Linux pour extraire des tarballs de sources- En mars 2024, une porte dérobée a été découverte dans
xz, et le mainteneur malveillant Jia Tan a gagné la confiance du projet pendant environ trois ans, obtenu les droits de push sur le dépôt, puis inséré progressivement du code obfusqué au milieu de contributions légitimes - Andres Freund, en enquêtant sur une dégradation de performance de 500 ms de
sshsur plusieurs machines Debian unstable, a remonté la piste jusqu’àliblzma, puis identifié et documenté la porte dérobée - L’objectif de la porte dérobée était de détourner
sshafin de permettre à l’attaquant d’exécuter des commandes arbitraires sur la machine victime lors d’une connexion avec une clé RSA spécifique
Le tarball malveillant et l’objet généré pendant le build
- Les composants malveillants n’étaient pas directement présents dans tout le code du dépôt Git de
xz, mais inclus dans les tarballs de releasexz5.6.0 et 5.6.1 buildés, signés et distribués par Jia Tan - Certains fichiers
.xzde test dans le dépôt Git contenaient du code machine dissimulé, et le tarball de release ajoutait des modifications déguisées destinées à l’en extraire - En particulier, la modification de
m4/build-to-host.m4paraissait inoffensive et même commentée, mais cachait en réalité une chaîne de commandes qui déchiffrait et désobfusquait plusieurs faux fichiers de test.xz - Ce processus produisait deux éléments
- un script shell exécuté pendant le build de
xz - un fichier objet binaire malveillant
- un script shell exécuté pendant le build de
- Le script shell exécuté pendant le build remplissait deux rôles
- vérifier les conditions d’activation de la porte dérobée, comme certaines distributions Linux, des fonctionnalités de
glibcou la présence dessh - modifier l’objet légitime
liblzma_la-crc64_fast.opour qu’il utilise le symbole_get_cpuiddu fichier objet de la porte dérobée
- vérifier les conditions d’activation de la porte dérobée, comme certaines distributions Linux, des fonctionnalités de
- L’
analyse du script xzde Russ Cox détaille la façon dont les ressources malveillantes sont créées pendant le build
Détournement d’ifunc menant à sshd
- Dans un programme lié dynamiquement, le chargeur dynamique charge les bibliothèques partagées en mémoire au moment de l’exécution et remplit la Global Offset Table (GOT) avec les adresses des symboles
- Les ifunc de
glibcservent à choisir, lors du chargement dynamique, l’une des différentes implémentations d’une même fonction ; la porte dérobéexzs’en servait comme chemin d’exécution de code malveillant - Sur certaines distributions comme Debian et Fedora,
sshest lié àlibsystemdpour prendre en charge les notificationssystemd, etlibsystemdest à son tour lié àliblzma- Dans cet environnement,
sshddépend transitivement deliblzma
- Dans cet environnement,
- Au lancement de
sshd, le chargeur dynamique chargelibsystemdetliblzma, et si la porte dérobée est installée, le code malveillant s’exécute pendant le chargement deliblzma - La porte dérobée force le résolveur ifunc à appeler le symbole malveillant
_get_cpuid, puis manipule la GOT, qui n’est pas encore en lecture seule, pour remplacer l’adresse deRSA_public_decryptpar celle d’une fonction malveillante - L’analyse détaillée de Securelist et un article de recherche résumant les vecteurs d’attaque et les mesures d’atténuation peuvent servir de références
Sources de confiance et contrainte de bootstrap de NixOS
- Cette attaque a pu être efficace parce que beaucoup de distributions buildaient
xzdepuis le tarball fourni par le mainteneur plutôt que depuis les sources originales d’une forge Git - Le workflow par tarball a des raisons historiques et pratiques
- c’est l’approche des premières distributions Linux, antérieure à
git - il conserve l’état des sources au moment de la release dans une archive autonome
- il peut inclure des artefacts intermédiaires comme les pages man ou les scripts configure, réduisant la charge de build
- la compression permet d’économiser de l’espace de stockage
- c’est l’approche des premières distributions Linux, antérieure à
- Du point de vue de la sécurité, lorsque c’est techniquement possible, il faut builder depuis des sources authentifiées par l’acteur le plus digne de confiance
- Pour un projet développé sur GitHub, on peut utiliser les archives générées automatiquement par GitHub pour chaque release
- Le même raisonnement peut s’appliquer à des plateformes tierces de confiance comme Codeberg, SourceHut ou GitLab
- NixOS utilise un modèle de gestion de paquets fonctionnel, où les paquets définissent leurs recettes de build sous forme d’expressions Nix
- Les mainteneurs de NixOS ont pour culture d’utiliser, quand c’est possible, les archives de sources générées automatiquement par GitHub via
fetchFromGitHub, mais le paquetxzrécupérait viafetchurlun tarball de mainteneur uploadé manuellement - La raison est que
xzfait partie de l’étape de bootstrap denixpkgs- Le bootstrap est le processus permettant de rebuilder l’ensemble des paquets
nixpkgsà partir d’un petit ensemble de seed binaries stdenvest l’environnement de compilation de base utilisé par les autres paquetsstdenvdépend dexzà l’exécution, et des paquets commecoreutilsont besoin dexzpour extraire des archives de sources.tar.xz
- Le bootstrap est le processus permettant de rebuilder l’ensemble des paquets
- Builder
xzdepuis les sources GitHub nécessiteautoconfpour générer le script configure, maisautoconfdépend lui aussi dexz- Le tarball fourni par le mainteneur contient déjà un script configure généré, ce qui permet de casser ce cycle de dépendance
- Ainsi, au point du graphe
nixpkgsoùxzest buildé, il était difficile d’utiliser l’archive de sources GitHub et il fallait faire confiance au tarball du mainteneur
Comparer la convergence des artefacts plutôt que les sources
- Comparer le tarball du mainteneur et le tarball de sources GitHub pour vérifier s’ils sont identiques semble naturel, mais fonctionne mal en pratique
- Daniel Stenberg explique que le fait qu’un tarball de release diffère des sources est une fonctionnalité
- il peut inclure des artefacts intermédiaires comme des pages man ou des scripts configure
- c’est particulièrement utile lorsque les distributions veulent éviter une dépendance à
autoconf
- Du point de vue de la sécurité de la chaîne d’approvisionnement, cette flexibilité devient une charge : il faut faire confiance à l’honnêteté du mainteneur
- Les builds reproductibles désignent la propriété selon laquelle deux builds réalisés dans les mêmes conditions produisent des artefacts identiques bit à bit
- Le groupe reproducible-builds vise à rendre reproductibles autant de paquets que possible, et Reproducible Builds: Increasing the Integrity of Software Supply Chains traite cette propriété comme un moyen d’accroître la confiance dans la distribution d’artefacts binaires
- L’approche consistant à rendre reproductible la génération même du tarball a été adoptée par le projet PostgreSQL
- Les utilisateurs peuvent vérifier indépendamment que le tarball du mainteneur a été généré honnêtement depuis les sources originales
- Toutefois, comme cela doit être implémenté par les mainteneurs upstream, l’adoption peut être lente dans la communauté FOSS
Procédure de détection proposée dans NixOS
- Si
xzest reproductible bit à bit et que le tarball du mainteneur ne contient aucune modification influençant le processus de build, alors le build depuis le tarball GitHub et celui depuis le tarball du mainteneur doivent produire les mêmes artefacts - La méthode proposée consiste à rebuilder
xzune fois le bootstrap terminé, cette fois en utilisant l’archive de sources GitHub viafetchFromGitHub, puis à comparer le résultat avec l’artefactxzissu de l’étape de bootstrap existante - L’exemple d’implémentation place
after-bootstrap.nixsouspkgs/tools/compression/xzet buildexz5.6.1 depuis les sources GitHub- Le tarball du mainteneur contient déjà le script configure, mais le build depuis les sources GitHub doit exécuter
./autogen.shavecautoconf,libtool,automake, etc.
- Le tarball du mainteneur contient déjà le script configure, mais le build depuis les sources GitHub doit exécuter
- Une étape de comparaison,
compareArtifacts, est ajoutée pour comparerliblzma.soà la fin du build- Dans l’exemple, seul
liblzma.so, le fichier modifié par la porte dérobée, est comparé, mais cela peut être généralisé à tous les artefacts binaires et bibliothèques
- Dans l’exemple, seul
- Sur une révision master normale, le build
xz-after-bootstrapa réussi et les deux artefacts étaient identiques - Sur la révision
c53bbe3contenant la porte dérobée, ils ont d’abord semblé identiques- La porte dérobée ne s’activait pas sur NixOS, notamment parce que le script d’installation vérifiait la définition de la variable
RPM_ARCH - Une fois la condition activée avec
env.RPM_ARCH = true;, une différence binaire dansliblzma.soest apparue
- La porte dérobée ne s’activait pas sur NixOS, notamment parce que le script d’installation vérifiait la définition de la variable
- En sauvegardant et comparant les deux
liblzma.so, l’artefact contenant la porte dérobée pesait 258 Ko, tandis que celui buildé après bootstrap depuis les sources GitHub pesait 210 Ko, soit environ 48 Ko d’écart - La comparaison des symboles avec
nma révélé l’ajout de_cpuid,_get_cpuidet__tls_get_addr@GLIBC_2.3,_get_cpuidétant un symbole documenté dans plusieurs rapports techniques sur la porte dérobéexz
Pistes d’application et limites
- La protection proposée consiste à appliquer cette méthode à tous les paquets buildés pendant l’étape de bootstrap qui n’utilisent pas d’archive de sources de confiance
- Ajouter les paquets
*-after-bootstrapaux channel blockers permettrait de transformer un échec de build en alerte majeure nécessitant l’intervention des mainteneurs - Une pull request a été ouverte dans le dépôt
nixpkgscomme preuve de concept pour le casxz - Cette méthode suppose que le paquet ciblé est reproductible bit à bit
- De 2017 à 2023, 17 révisions de
nixpkgs-unstableont été échantillonnées, et les non-fixed-output derivations destdenvont été rebuilées avecnix-build --check - Dans toutes les révisions,
xzétait reproductible bit à bit - Dans 12 des 17 révisions, un ou deux paquets pouvaient être buildés mais n’étaient pas reproductibles ; par exemple,
gccétait systématiquement non reproductible de 2017 à 2021, etbashjusqu’en 2019
- De 2017 à 2023, 17 révisions de
- Cette méthode ne peut pas être appliquée à tous les paquets de
stdenv, mais elle peut être activée sélectivement à long terme pour les paquets qui affichent une bonne reproductibilité - Le problème du trusting trust de Ken Thompson reste une limite théorique de cette approche
- Il faut supposer que le
xznon fiable de l’étape de bootstrap ne peut pas contaminer indirectement le build dexz-after-bootstrapde façon à faire paraître les artefacts identiques - Une telle attaque pourrait être très complexe, mais la méthode repose bien sur cette hypothèse
- Il faut supposer que le
- Cette technique de détection ne repère une modification du tarball que lorsqu’elle affecte l’artefact final
- Comme les exécutables NixOS ne contenaient pas la porte dérobée, ce cas n’aurait pas été détecté si la condition n’avait pas été activée séparément
1 commentaires
Avis sur Hacker News
Il faut souligner que NixOS et les builds reproductibles n’ont pas détecté la porte dérobée de xz. En réalité, NixOS a lui aussi distribué à ses utilisateurs une build malveillante de xz ; c’est seulement que le malware ne ciblait pas NixOS et ne s’y est donc pas exécuté.
Un développeur de NixOS a également déclaré : « quand la porte dérobée a été rendue publique, j’ai été surpris d’apprendre qu’une version malveillante de xz avait été distribuée à nos utilisateurs ». Comme toujours, la théorie et la réalité diffèrent, et ce qui a rendu xz possible relevait moins d’une faille technique que de vulnérabilités humaines du monde réel. La communauté a du mal à admettre que ce genre de problème ne peut pas toujours être corrigé uniquement avec de meilleurs logiciels.
Si l’on pouvait exécuter chaque processus avec des droits limités et l’empêcher d’accéder même à
~/, sauf aux répertoires nécessaires à sa tâche, on pourrait par exemple empêcher un paquetpipmalveillant de voler des clés SSH. Cela dit, je pense que si la porte dérobée de xz n’a pas fonctionné sur NixOS, c’est à cause de la structure de système de fichiers non FHS particulière de NixOS.De nos jours, la réalité concrète est souvent dénigrée quand elle ne correspond pas aux émotions ou aux valeurs personnelles que l’on associe à la réalité objective. J’ai mes propres valeurs, mais je ne considère pas que la réalité concrète soit moins importante que ce que je ressens à son sujet. Comme on le dit souvent, la différence entre la théorie et la pratique, c’est qu’en théorie elles sont identiques, mais pas en pratique. J’espère que le fait de comprendre que les builds reproductibles de NixOS auraient pu détecter l’attaque xz mais ne l’ont pas fait mènera à des progrès permettant d’analyser ces builds et de trouver plus rapidement d’autres attaques à l’avenir.
J’ai l’impression que l’auteur a une vision trop focalisée sur la manière dont les choses se sont produites par hasard cette fois-ci. L’affaire Jia Tan ne constitue qu’un seul échantillon ; considérer que seule cette méthode est possible est myope.
On peut facilement imaginer de nombreux scénarios dans lesquels les défenses proposées ici ne fonctionneraient pas. Même en tant qu’utilisateur de Nix, je pense qu’il est peu probable que NixOS l’aurait détecté. De fait, il ne l’a pas détecté. Je viens certes de dire que la prochaine fois pourrait se produire différemment, mais faire confiance à Nix sans preuve serait aussi stupide.
Ici, NixOS n’est pas vraiment pertinent. La porte dérobée de xz ciblait précisément Red Hat et Debian.
Avec la même logique, dire que la porte dérobée de xz n’a pas affecté Windows serait tout aussi pertinent. Ironiquement, cette porte dérobée a finalement été découverte par un employé de Microsoft, un point souvent négligé.
L’article dit que les distributions devraient récupérer le code source directement depuis un système de gestion de versions, par exemple Github, plutôt que depuis les tarballs traditionnels destinés à l’installation.
Mais je ne vois pas bien ce que cela résout. Un mainteneur malveillant n’aurait-il pas simplement à ajouter directement un blob binaire dans le dépôt de code source ? L’auteur semble considérer Github comme fiable, comme s’il vérifiait le code, mais bien sûr Github ne fait pas ce type de vérification.
Des builds reproductibles vérifiés auraient pu aider à empêcher des cas comme la compromission de xz utils ou la modification de SolarWinds Orion, et cela vaut largement la peine d’être fait.
Si vous téléchargez un binaire depuis une release Github et l’exécutez, vous n’avez pas d’autre choix que de faire entièrement confiance au mainteneur. Nix ne fait cela que pour les paquets closed source.
L’argument repose sur le fait que le mainteneur de XZ a caché du code malveillant dans un tarball qui n’était pas enregistré dans Git.
L’auteur montre que Nix peut être configuré pour générer un tarball depuis Git et l’inclure dans la build binaire. Mais je ne vois pas pourquoi ce serait une fonctionnalité propre à Nix ou NixOS. N’importe quel système de build, y compris ceux qui produisent des RPM ou des Deb, peut être configuré pour générer un tarball comme étape intermédiaire. En réalité, Debian travaille depuis longtemps sur les builds reproductibles comme objectif important. https://wiki.debian.org/ReproducibleBuilds
Premièrement, le processus de build de NixOS avait besoin de xz trop tôt dans le bootstrap pour pouvoir effectuer une build complète depuis les sources de xz. Deuxièmement, il propose des ajustements à nixpkgs pour détecter automatiquement si des dépendances de nixpkgs nécessaires tôt dans le bootstrap ont été compromises. D’autres écosystèmes peuvent bien sûr tenter des builds complètes depuis les sources pour repérer des divergences. Le point central de l’article est que nixpkgs ne peut pas le faire actuellement.
Si l’on veut se concentrer sur un cas que NixOS aurait pu empêcher, il faut regarder l’incident CrowdStrike. Le simple fait de pouvoir démarrer sur la configuration d’hier si celle d’aujourd’hui ne fonctionne pas aurait atténué la plupart des dégâts.
Si vous utilisez un framework de confiance, vous êtes en sécurité jusqu’à ce que ce framework soit attaqué. La backdoor xz a peut-être été découverte, mais elle n’avait pas été conçue dans le but de fonctionner dans l’écosystème Nix.
Le jour où un développeur cœur de Nix sera un espion, ou dans une situation similaire, il y aura aussi des attaques visant l’écosystème Nix. J’aimerais qu’on évite de répondre comme si Nix était intrinsèquement sûr. Si Nix est attaqué avec succès d’ici un ou deux ans, j’aurai presque envie d’aller vous voir pour vous faire admettre que vous aviez tort.
L’affirmation plus forte est que « Nix est plus difficile et plus coûteux à attaquer que les systèmes de build traditionnels ». Donc, si vous trouvez un moyen d’attaquer Nix à moindre coût, revenez à ce moment-là. D’ici là, dire que Nix est plus difficile à attaquer que les systèmes alternatifs est au moins plausible sur le plan technique, et très probablement vrai en pratique.
NixOS va un peu plus loin, mais la plupart des autres distributions compilent aussi tout depuis les sources, vérifient cryptographiquement que les sources utilisées n’ont pas été altérées, et ont des dépendances versionnées entre paquets. Debian dispose aussi de builds reproductibles.
Le problème venait du fait que le système de build ne supprimait pas les fichiers objets précompilés avant de compiler depuis les sources. Même en corrigeant cela, si personne n’inspecte le code source, on peut y ajouter autant de backdoors qu’on veut, et ni NixOS ni aucune autre distribution ne pourra l’empêcher.
Excellente analyse technique, mais le titre est faux et trompeur. On pourrait dire qu’il est « techniquement correct », mais au mieux il s’agit plutôt du sens dans lequel la backdoor a été implantée.
Cela montre bien qu’il faut des outils de gestion de build qui aillent au-delà des couches de systèmes de fichiers en union. Par exemple, ils devraient tracer et imposer que les tests ne puissent pas contaminer les artefacts de build. Il faudrait construire un graphe de traçage causal des fichiers qui influencent d’autres fichiers pendant le build, rendre ce graphe explicite, puis l’imposer ou signaler les différences avec le graphe de traçage précédent.
Exact. Il aurait certainement été plus difficile de cacher la backdoor. Mais on est loin de l’impossible.
Si on le souhaite, on peut toujours cacher une backdoor dans le code source. Cela demande simplement plus d’efforts pour la faire passer pour un bug plausible, et augmente aussi les chances qu’elle soit détectée.