Xz/liblzma : explication de l’obfuscation à l’étape Bash
(gynvael.coldwind.pl)- 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 avecxz -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
.odans 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.xztests/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,tretxz -dpour extraire le script Stage 1 - Après lecture des octets de
tests/files/bad-3-corrupt_lzma2.xz, certains sont remplacés viatr "\t \-_" " \t_\-"0x09tabulation devient0x20espace0x20espace devient0x09tabulation0x2dtiret devient0x5funderscore0x5funderscore devient0x2dtiret
- Cette substitution sert à retransformer
bad-3-corrupt_lzma2.xzen 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
- 5.6.0 :
- Dans la 5.6.1, une vérification Linux de la forme
[ ! $(uname) = "Linux" ] && exit 0a é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 viaeval $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.lzmaest décompressé avecxz -dc eval $irépète des appels àheadselon 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
- D’abord,
- 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 +31233supprime 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
trest 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"
- 5.6.0 :
- 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.txtjoint 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
~!:_ Wet|_!{ - - Le Fragment 3 recherche
jV!.^%et%.R.1Z
- Le Fragment 1 recherche
- 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é
trutilisée dans la 5.6.0, puis décompressées avecxz -F raw --lzma2 -dcavant d’être exécutées viaeval - 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
.opour 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.lzmaest décompressé avecxz -dc- La chaîne
headdeeval $iutilisé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
- Le tableau de clé est généré par
- Les données déchiffrées sont ensuite décompressées avec
xz -dc --single-stream - Enfin,
headdécoupe uniquement la plage d’octets nécessaire, qui est enregistrée sousliblzma_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
sedtrxzheadtailawkgrepcutexpr
- 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
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
#includehttps://git.tukaani.org/?p=xz.git;a=commitdiff;h=328c52da8a2...
https://old.reddit.com/r/linux/comments/1brhlur/xz_utils_bac...
+,+.,+, et ce simple point ne se remarque pasRien 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
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.pngCe 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
Le prochain attaquant sera beaucoup moins négligent au point de laisser des traces comme une augmentation du temps d’exécution
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
bad-3-corrupt_lzma2.xzcontient 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-streamLes 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.xzCette 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
Un énorme fichier
.shaffiche la licence et demande l’accord, puis fait uncatde lui-même et passe par un pipelinehead/tailverscpiopour extraire les vrais assetsJe 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.
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
Un nombre non négligeable d’outils sont construits de cette manière
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
execquelque chose ? Je me dis qu’un « mode sécurisé » dans bash serait peut-être utileCela 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
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
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
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
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-currentn’était pas affectéeSi 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
-currentne lie passshdà xz et n’utilise pas systemd