1 points par GN⁺ 2024-03-31 | 1 commentaires | Partager sur WhatsApp
  • La porte dérobée xz/liblzma ne se limite pas au payload binaire final : l’étape Bash exécutée pendant la compilation est elle aussi dissimulée derrière plusieurs couches d’extraction et de déchiffrement, ce qui rend l’analyse difficile
  • Les versions affectées sont xz/liblzma 5.6.0 et 5.6.1, et le script obfusqué comme le payload binaire sont cachés dans deux fichiers qui ressemblent à des fichiers de test
  • Le Stage 0 commence dans m4/build-to-host.m4, restaure un flux xz qui semble corrompu, puis extrait et exécute le script Stage 1 avec xz -d
  • Le Stage 1 découpe et remplace des données depuis good-large_compressed.lzma, puis exécute le script Bash Stage 2 ; dans la 5.6.1, du code a été ajouté pour vérifier 5 fois que l’exécution se fait bien sous Linux
  • Le Stage 2 utilise du file carving, un chiffrement par substitution et un déchiffrement dérivé de RC4 basé sur AWK pour injecter un fichier .o dans le processus de build et de linkage ; la 5.6.1 inclut aussi une structure extensible permettant d’exécuter plus tard des scripts supplémentaires

Portée de l’étape Bash de la porte dérobée xz/liblzma

  • Andres Freund a annoncé sur la mailing-list oss-security la découverte de la porte dérobée xz/liblzma, qui affecte le serveur OpenSSH
  • L’objet de l’analyse n’est pas la porte dérobée binaire elle-même, mais les premières étapes en Bash qui l’exécutent en amont, ainsi que leurs mécanismes d’obfuscation
  • Le flux Bash s’étend du Stage 0 au Stage 2, avec aussi quelques indices d’un éventuel Stage 3 qui semble ne pas avoir été complètement implémenté
  • Les étapes obfusquées et chiffrées, ainsi que la porte dérobée binaire ultérieure, sont cachées dans deux fichiers de test
    • tests/files/bad-3-corrupt_lzma2.xz
    • tests/files/good-large_compressed.lzma
  • Les versions affectées sont 5.6.0 et 5.6.1, avec de petites différences entre les deux

Stage 0 : restauration d’un flux xz déguisé en fichier de test corrompu

  • Le point de départ est du code dans m4/build-to-host.m4, qui semble être exécuté à un moment du processus de build
  • Le pipeline central enchaîne sed, tr et xz -d pour extraire le script Stage 1
  • Après lecture des octets de tests/files/bad-3-corrupt_lzma2.xz, certains sont remplacés via tr "\t \-_" " \t_\-"
    • 0x09 tabulation devient 0x20 espace
    • 0x20 espace devient 0x09 tabulation
    • 0x2d tiret devient 0x5f underscore
    • 0x5f underscore devient 0x2d tiret
  • Cette substitution sert à retransformer bad-3-corrupt_lzma2.xz en flux xz valide
  • À la fin, le flux restauré est décompressé par xz ; les erreurs sont ignorées, le script Stage 1 est récupéré puis exécuté immédiatement
    • Le flux semble tronqué, mais la sortie utile a déjà été écrite, donc cela ne pose pas de problème

Stage 1 : extraire le Stage 2 depuis good-large_compressed.lzma

  • Le Stage 1 est un court fichier Bash commençant par ####Hello####, mentionné dans le mail d’Andres Freund
  • La première différence entre les deux versions est une suite d’octets arbitraires dans le commentaire de la deuxième ligne
    • 5.6.0 : 86 F9 5A F7 2E 68 6A BC
    • 5.6.1 : E5 55 89 B7 24 04 D8 17
  • Dans la 5.6.1, une vérification Linux de la forme [ ! $(uname) = "Linux" ] && exit 0 a été ajoutée, et le même test est répété 5 fois
  • L’extraction du Stage 2 commence par une longue ligne export i=..., dont la valeur est ensuite utilisée comme une sorte de fonction appelée via eval $i
  • Le flux d’extraction repose sur une structure en pipeline qui réenchaîne continuellement sortie standard et entrée standard
    • D’abord, good-large_compressed.lzma est décompressé avec xz -dc
    • eval $i répète des appels à head selon un motif qui ignore 1024 octets puis en émet 2048
    • En fin de fichier, la 5.6.0 émet 724 octets et la 5.6.1 en émet 939
  • Cette chaîne de head élimine les zones vides à faible entropie et ne recolle que les régions qui contiennent les vraies données
  • Ensuite, tail -c +31233 supprime la partie initiale
    • Cette partie contient la porte dérobée binaire qui sera extraite à l’étape suivante
    • En pratique, cela revient à écarter les 31264 premiers octets dans la 5.6.0 et les 31232 premiers dans la 5.6.1
  • Puis tr est utilisé comme un simple chiffrement par substitution, avec une clé de correspondance différente entre 5.6.0 et 5.6.1
    • 5.6.0 : tr "\5-\51\204-\377\52-\115\132-\203\0-\4\116-\131" "\0-\377"
    • 5.6.1 : tr "\114-\321\322-\377\35-\47\14-\34\0-\13\50-\113" "\0-\377"
  • Les données substituées sont ensuite à nouveau décompressées avec xz -F raw --lzma1 -dc, et le Stage 2 obtenu est exécuté immédiatement

Stage 2 : modification du processus de build et mécanisme d’extension

  • Le Stage 2 correspond au fichier infected.txt joint par Andres Freund dans son mail d’origine ; c’est le script Bash où se produit la modification effective de la compilation
  • Du point de vue de l’obfuscation, le Stage 2 contient trois fragments intéressants, dont deux n’apparaissent que dans la 5.6.1
  • Les deux fragments de la 5.6.1 sont presque identiques et cherchent dans tests/files/ des fichiers contenant certaines signatures
    • Le Fragment 1 recherche ~!:_ W et |_!{ -
    • Le Fragment 3 recherche jV!.^% et %.R.1Z
  • Si une signature est trouvée, le nom du fichier et l’offset sont extraits, puis la plage d’octets située après la première signature et avant la seconde est découpée
  • Les données découpées sont déchiffrées par substitution avec la clé tr utilisée dans la 5.6.0, puis décompressées avec xz -F raw --lzma2 -dc avant d’être exécutées via eval
  • Aucun fichier portant ces signatures n’a été trouvé dans les archives TAR 5.6.0 et 5.6.1 examinées
  • Cette structure ressemble à un système d’extension et de patch permettant d’exécuter de futurs scripts en ajoutant simplement un nouveau fichier de test, sans avoir à modifier à nouveau les fichiers de test existants qui transportent déjà le payload

Stage 2 : extraction du fichier .o de la porte dérobée binaire

  • Un autre élément central du Stage 2 est le code qui extrait un fichier .o pour l’insérer dans le processus de compilation et de linkage
  • Le résultat extrait est liblzma_la-crc64-fast.o, qui constitue la porte dérobée binaire finale
  • Le processus d’extraction relie plusieurs outils standards en pipeline, comme dans les étapes précédentes
    • good-large_compressed.lzma est décompressé avec xz -dc
    • La chaîne head de eval $i utilisée au Stage 1 est réemployée
    • sed "s/\(.\)/\1\n/g" ajoute un retour à la ligne après chaque octet afin d’obtenir un flux d’une ligne par octet
  • Le script AWK qui suit applique au flux d’entrée un déchiffrement proche de RC4
    • Le tableau de clé est généré par ((i * 7) + 5) % 256
    • La 5.6.0 ignore les 4096 premiers octets du flux PRNG de sortie
    • La 5.6.1 ignore les 8192 premiers octets
    • Le RC4 original utilise XOR, mais ce code emploie une addition sous la forme (v + k) % 256
  • Les données déchiffrées sont ensuite décompressées avec xz -dc --single-stream
  • Enfin, head découpe uniquement la plage d’octets nécessaire, qui est enregistrée sous liblzma_la-crc64-fast.o
    • La taille du payload compressé mais partiellement transformé est de 88792 octets dans la 5.6.0 et de 88664 octets dans la 5.6.1
    • La plage finale extraite représente environ 86 KB

Nature de l’obfuscation construite uniquement avec des outils standards

  • L’ensemble de l’étape Bash repose uniquement sur des outils en ligne de commande standards
    • sed
    • tr
    • xz
    • head
    • tail
    • awk
    • grep
    • cut
    • expr
  • Le payload est caché dans des fichiers binaires qui ressemblent à des fichiers de test normaux, et seules les zones nécessaires sont récupérées par file carving
  • L’obfuscation combine un simple chiffrement par substitution et un déchiffrement dérivé de RC4 implémenté en AWK
  • L’exécution se déroule en trois étapes au total, et la 5.6.1 inclut une structure permettant d’exécuter plus tard des scripts en ajoutant un fichier de test séparé
  • L’ensemble montre un effort important pour paraître inoffensif et rester bien dissimulé ; si cette affaire a été découverte par hasard, cela soulève la question du nombre de cas similaires encore non détectés

1 commentaires

 
GN⁺ 2024-03-31
Avis de Hacker News
  • Grâce à l’explication simplifiée et à la comparaison avec une image bruitée, on comprend mieux ce que les gens entendent par sophistication
    J’ai aussi vu sur reddit que le mécanisme de « sandboxing » avait été cassé par un simple point, et on voit ce point tout à gauche de la ligne juste après #include
    https://git.tukaani.org/?p=xz.git;a=commitdiff;h=328c52da8a2...
    https://old.reddit.com/r/linux/comments/1brhlur/xz_utils_bac...

    • C’est vraiment vicieux. Dans le diff, ça ressemble juste à +, +., +, et ce simple point ne se remarque pas
    • Je déteste vraiment l’usage de ce genre de conditions au moment de la compilation/du build. Il est difficile de tester qu’elles s’activent quand elles doivent l’être et se désactivent quand elles doivent l’être, surtout quand elles sont dans un système de build sans framework de tests unitaires
      Rien qu’une simple erreur qui fait que les tests échouent toujours ou réussissent toujours est déjà pénible ; on voit bien pourquoi c’est une bonne cible pour un comportement malveillant
    • Il est très probable que ce soit une simple erreur, et non quelque chose d’intentionnel
      a) Pratiquement personne ne build ce paquet avec cmake
      b) Si on build avec cmake et -DENABLE_SANDBOX=landlock, le build échoue tout simplement : https://i.imgur.com/7xbeWFx.png
      Ce point ne désactive pas le sandboxing ; il empêche simplement le build avec cmake. Si quelqu’un avait réellement essayé de builder avec cmake, il aurait vu l’erreur et compris que quelque chose n’allait pas, donc l’hypothèse d’une tentative malveillante de réduire la sécurité n’a pas de sens
  • La question la plus importante est : « si cela a été découvert par hasard, combien de choses restent encore à découvrir ? »
    Il est peu probable qu’Andres Freund soit tombé par hasard sur l’unique projet open source populaire dans lequel une backdoor avait été introduite. Il ne serait pas surprenant qu’il y en ait déjà une douzaine de ce genre dans la nature

    • Ce n’est pas tellement qu’il l’a découvert par hasard ; c’est plutôt qu’il l’a senti. Ce sont deux choses différentes
      Le prochain attaquant sera beaucoup moins négligent au point de laisser des traces comme une augmentation du temps d’exécution
    • C’est possible, mais il se peut aussi qu’en pratique il n’y ait pas beaucoup de cas critiques. S’il y en avait beaucoup, j’ai l’impression qu’on rencontrerait ce genre de situation plus souvent
  • L’idée dérangeante ici, c’est que les tests unitaires ont ouvert la voie à l’attaque. Sans tests, cela aurait été beaucoup plus difficile à cacher

    • L’attaquant a même masqué les traces du payload initial derrière un paragraphe inoffensif du README, du genre « rien à voir ici ! »
      bad-3-corrupt_lzma2.xz contient trois flux, le premier et le troisième étant des flux xz valides. Il y est écrit que le flux du milieu possède un en-tête de flux, un en-tête de bloc, un index et un pied de flux corrects, que seules les données LZMA2 sont corrompues, et qu’il devrait être décompressé avec --single-stream
      Les chaînes ####Hello#### et ####World#### produisent un résultat normal crédible quand on suit réellement les instructions du README
      $ cat tests/files/bad-3-corrupt_lzma2.xz | xz -d --single-stream
      ####Hello####
      Ces chaînes sont des commentaires shell, elles ne gênent donc pas l’exécution du payload
      Enfin, elles servent ensuite de marqueurs permettant à l’expression régulière de retrouver le fichier sans référencer directement son nom ni utiliser les vraies chaînes Hello/World
      $ gl_am_configmake=\grep -aErls "#{4}[[:alnum:]]{5}#{4}$" $srcdir/ 2>/dev/null``
      $ echo $gl_am_configmake
      ./tests/files/bad-3-corrupt_lzma2.xz
    • Pour un projet critique en matière de sécurité, il semblerait judicieux de configurer l’infrastructure de build pour produire une erreur, ou au moins un avertissement, lorsqu’un fichier binaire est inclus dans le build
      Cette vérification devrait s’appliquer transitivement, de sorte que lorsqu’une distribution Linux tente de passer à une nouvelle version de liblzma, le build échoue ou émette un avertissement à cause de la nouvelle dépendance binaire
      Je ne sais pas à quel point cette pratique est courante dans les builds des distributions Linux. Si elle l’est, la remise en ordre demanderait un travail énorme, et il n’est même pas certain que ce soit possible au départ. Avec bazel, cela semble possible, mais je connais moins les autres systèmes de build
  • Je me demande si quelqu’un a déjà cherché sur GitHub des astuces head | tail similaires. Il est difficile de croire que cela ait été inventé spécialement pour l’occasion

    • J’ai assez souvent vu cette méthode dans des installateurs Unix de logiciels commerciaux
      Un énorme fichier .sh affiche la licence et demande l’accord, puis fait un cat de lui-même et passe par un pipeline head/tail vers cpio pour extraire les vrais assets
    • C’est malin, mais ce n’est pas totalement nouveau ; c’est assez proche des cas d’usage prévus pour ce genre d’outils
    • Une bonne occasion d’écrire un article de recherche
  • Je n’ai pas de meilleure réponse, mais ce fatras de bash obscur ne sent-il pas mauvais en lui-même ?
    Comme dans d’autres domaines du monde du développement, n’aurait-on pas pu l’écrire de manière moins opaque, pour que ce qui se passe soit plus clair ?
    Je sais qu’un mainteneur peut introduire du code malveillant sans être soumis à un examen aussi strict qu’un contributeur externe, mais il doit bien y avoir une meilleure voie que ce tas de code « concis », qui ressemble en pratique à de l’obfuscation involontaire.

    • C’est une très vieille mauvaise odeur
      Le problème de fond, c’est que dans les années 80 et 90 il existait une multitude de systèmes de type Unix, chacun avec ses défauts et ses fonctionnalités manquantes, et les auteurs de logiciels voulaient minimiser les dépendances de build
      Beaucoup de communautés se sont donc standardisées sur l’automatisation des builds avec des scripts shell fonctionnant partout. Mais écrire des scripts shell était pénible, et les gens ont fini par générer ces scripts avec des outils comme le préprocesseur de macros M4
      Résultat : beaucoup de projets se retrouvent avec d’énormes blocs de scripts shell opaques, au cas où quelqu’un voudrait exécuter le code sur AIX ou sur un antique Unix cassé
      Pour se débarrasser de ce maquis de shell impénétrable, il faudrait réduire fortement le nombre de plateformes prises en charge, se standardiser sur des outils de build plus propres, et construire davantage d’infrastructure de base dans des langages qui n’ont pas besoin du shell pour des builds portables
      Mais c’est un travail énorme, et une bonne partie des bibliothèques C fondamentales sont maintenues par un ou deux bénévoles non rémunérés. Abandonner la prise en charge de « Obscurnix-1997 » devient aussi, en général, une décision assez controversée
      C’est pourquoi une grande partie de l’infrastructure de base reste entourée d’un marécage de scripts shell générés par une machine et impossibles à identifier clairement
    • Ce shell n’a pas été écrit par un humain, c’est du code généré. Comme autoconf est très répandu, il existe des montagnes de code de configuration shell généré, et autoconf produit, à partir de scripts de préprocesseur de macros M4, des milliers de lignes de scripts shell portables difficiles à lire
      Un nombre non négligeable d’outils sont construits de cette manière
    • À première vue, le simple fait que du bash ait l’air obscur ne me paraît pas être un signal d’alerte. Des scripts bash écrits de façon très dense peuvent parfois ressembler à ça. La question de savoir s’il faut les écrire de manière aussi dense est un autre débat
      Ce qui paraît suspect, ce sont les appels répétés à tr. Quand je vois ça, je me dis que quelqu’un essaie d’être malin, et ici « malin » est à prendre au sens négatif. Si j’étais mainteneur, j’aurais demandé qu’on m’explique ce que fait ce code en arrivant. Il existe presque toujours une meilleure solution que ce genre de chaînage
      Le vrai problème, c’est qu’il n’y avait pas d’autre mainteneur pour examiner ce code au moment où il est arrivé. Un composant important de la pile dépendait d’une seule personne, et dans ce cas précis, cette personne était malveillante
    • Ce n’était pas « involontaire » : le point central, c’est que c’était délibérément obfusqué
    • J’ai peut-être mal compris le risque de la backdoor XV, mais existe-t-il un moyen d’exécuter bash en l’empêchant d’exec quelque chose ? Je me dis qu’un « mode sécurisé » dans bash serait peut-être utile
      Cela dit, je n’arrive pas à imaginer comment le script configure de xv pourrait lancer bash dans un tel « mode sécurisé » hypothétique, donc j’annule
  • https://github.com/tukaani-project/.github/issues/2

    • C’est assez drôle. Non seulement c’est une horrible backdoor intentionnelle, mais comme la backdoor est une œuvre dérivée, que le source n’était pas inclus et qu’elle n’a pas été distribuée sous la « forme préférée pour les modifications », c’est aussi une violation de la GPL
  • Tout l’écosystème C, y compris les outils de build et les vieux utilitaires Unix, est un champ de mines sécuritaire qui n’attend que d’être exploité, et il finira par l’être
    Il suffit de voir à quel point il est facile de tout casser avec un simple point. Les gens doivent maintenant comprendre qu’on ne peut plus faire reposer la sécurité du monde sur C
    J’aimerais qu’on utilise Ada ou Rust avec des chaînes d’outils modernes

    • Ajouter un point en Rust ne casse rien ?
  • Je ne comprends pas comment cela a pu passer la revue de code et être fusionné. Sauf si j’ai raté quelque chose, ça paraît absurdement négligent

    • L’acteur malveillant était co-mainteneur du dépôt, il avait été plus actif que le mainteneur d’origine pendant un certain temps et disposait des droits de commit complets. Il a commité directement sur master, sans PR ni revue
      En plus, c’était fortement obfusqué dans un fichier binaire marqué comme fichier de test, c’est-à-dire dans des fichiers de test compressés xz « good »/« bad ». Si l’on ne savait pas quoi chercher, il était impossible de le remarquer
    • Le message de commit du fichier de test prétendait qu’il avait été généré par un générateur de nombres aléatoires. La personne qui a créé le tarball de release a inséré la dernière ligne au bon endroit, mais cela n’a pas été enregistré dans le dépôt
    • Il n’y a pas de revue de code sur un paquet qui n’a qu’un seul mainteneur actif
  • Utiliser une distribution LTS peut offrir une certaine protection. Slackware semble utiliser lzma, c’est-à-dire tar.xz, pour ses paquets, mais la dernière version stable hors -current n’était pas affectée
    Si l’on veut aller un cran plus loin du côté du logiciel libre, Hyperbola GNU n’était pas affectée non plus
    En plus, Slackware -current ne lie pas sshd à xz et n’utilise pas systemd