Une faille critique dans le langage Rust, Windows trinque
De la rouille, des fenêtres, une rustine
Une faille critique de sécurité a été trouvée dans la bibliothèque standard Rust. Elle ne peut être exploitée que sous Windows, à cause de la manière particulière dont le système de Microsoft découpe et interprète les arguments d’une commande.
Le 12 avril à 17h02
5 min
Logiciel
Logiciel
La découverte a été faite par le chercheur en sécurité RyotaK, qui l’a signalée à l’équipe de développement de Rust. Elle est désormais référencée comme CVE-2024-24576 et a reçu la note maximale de sévérité, soit 10 sur 10. Elle a bien sûr reçu un petit nom pour l’occasion : BatBadBud.
Cette vulnérabilité est présente dans la bibliothèque standard de Rust, cette dernière n’échappant pas correctement les paramètres quand des commandes (batch ou cmd) sont invoquées.
En cas d’exploitation, le risque est que des commandes arbitraires soient transmises. Bien que la faille soit bien celle de Rust, elle remet sur le devant de la scène la manière dont Windows exécute les commandes et lit les arguments qui les accompagnent. En effet, comme expliqué par d’autres, Rust n’est pas le seul langage avec lequel il faut faire attention dans ce contexte.
Ce qui se passe sous Windows
Comme indiqué le 9 avril dans un billet de blog, le groupe de travail Rust Security Response a été mis au courant du problème. Quand des fichiers bat ou cmd sont invoqués depuis une application Rust via l’API Command de Windows, il existe une possibilité qu’un acteur malveillant puisse glisser des arguments spécialement conçus pour exploiter la faille.
« Un attaquant capable de contrôler les arguments transmis au processus créé pourrait exécuter des commandes shell arbitraires en contournant l'échappement », explique ainsi Pietro Albini, l’un des membres du groupe de travail.
Pourquoi un tel problème ? Parce que l’invite de commande de Windows a sa propre logique de séparation des arguments. Une logique différente de celle trouvée dans les API Command::arg et Command::args de la bibliothèque standard de Rust.
Ces dernières permettent normalement de faire passer des arguments en toute sécurité. Mais, « la mise en œuvre est plus complexe que sur les autres plateformes, car l'API Windows ne fournit qu'une seule chaîne de caractères contenant tous les arguments du processus créé », explique Albini. Or, c’est à ce processus créé qu’incombe alors la tâche de diviser les arguments.
Dans ce contexte, l’équipe de sécurité considère que la logique d’échappement de Rust n’est pas « suffisamment rigoureuse ». En conséquence, Chris Denton, l’un des contributeurs de Rust, a développé un correctif pour atténuer – et non supprimer – le problème. Dans la version 1.77.2, des améliorations sont portées au code d’échappement, afin notamment que l’API Command renvoie une erreur (InvalidInput) quand les arguments ne sont pas échappés de manière sécurisée.
Toutes les versions antérieures sont considérées comme vulnérables.
Le problème est présent dans d'autres langages
RyotaK, le découvreur de la faille, a expliqué que d’autres langages peuvent rencontrer le même problème, toujours à cause de la manière dont Windows divise les arguments d’une commande.
Haskell a ainsi déjà son correctif, tandis que Node.js et PHP travaillent sur les leurs. Erlang, Go, Python et Ruby ont mis à jour leur documentation pour sensibiliser les développeurs sur ce sujet. Il est probable que la couverture médiatique de la faille aidera d’ailleurs à mieux les avertir.
Pour le chercheur, le problème de fond réside bien dans Windows, même si les failles de sécurité se trouvent bien dans les langages.
Il donne plusieurs conseils généraux, comme toujours spécifier l’extension de fichier de la commande dans les arguments. Il recommande également de toujours échapper les données contrôlées par l’utilisateur avant de les utiliser comme arguments.
« Pour éviter l'exécution inopinée de fichiers batch, vous devriez envisager de déplacer les fichiers batch dans un répertoire qui n'est pas inclus dans la variable d'environnement PATH. Dans ce cas, les fichiers batch ne seront pas exécutés si le chemin complet n'est pas spécifié, ce qui permet d'éviter l'exécution inopinée de fichiers batch », ajoute le chercheur.
Pourquoi axer la communication sur Rust ?
On peut se poser la question, de multiples langages étant concernés. D’abord, c’est dans Rust que la faille a été découverte. Son fonctionnement a incité d’autres chercheurs et développeurs à analyser le comportement dans d’autres langages.
Ensuite, Rust attire énormément l’attention depuis quelques années, particulièrement depuis qu’il est pris au sérieux par des entreprises comme Google et Microsoft. La première a ainsi investi un million de dollars dans la fondation Rust, tandis que la seconde embauche des développeurs Rust et compte réécrire tout ou partie du noyau Windows dans ce langage. La Maison-Blanche a même exhorté les entreprises technologiques à se pencher sur Rust.
Pourquoi un tel enthousiasme ? Parce que le langage, créé initialement par Mozilla (plus précisément par Graydon Hoare comme projet personnel), a fait ses preuves. C’est essentiellement parce qu’il est memory-safe tout en présentant de bonnes performances, proches de ce que l’on trouve en C et C++. Il est en tête du classement des langages les plus appréciés depuis plusieurs années sur StackOverflow.
Une faille critique dans le langage Rust, Windows trinque
-
Ce qui se passe sous Windows
-
Le problème est présent dans d’autres langages
-
Pourquoi axer la communication sur Rust ?
Commentaires (34)
Vous devez être abonné pour pouvoir commenter.
Déjà abonné ? Se connecter
Abonnez-vousLe 12/04/2024 à 17h08
Le 12/04/2024 à 17h51
Alors non, le problème ne vient pas de Windows. Le problème vient de la différence de comportement entre le monde Unix et le monde Windows (plus particulièrement, les caractères d'échappement à utiliser lors de la création d'un processus, où, sous Linux les spawn, popen, etc. utilise le backslash tandis que CreateProcess (l'API WIndows donc) utilise le caret (^).
Le fait que la même erreur ait été faite par plusieurs équipes distinctes dans plusieurs langages n'en fait pas un problème de Windows. Quand on appelle mal une API, il ne faut pas rejeter la faute sur l'API.
Des comportements Unix ont été calqués sur Windows, et après on vient dire que c'est de la faute de Windows. Le problème aurait tout aussi bien pu être dans l'autre sens, et là, personne n'aurait remis en cause Linux.
Comme expliqué dans l'article, on peut regretter que la communication soit axée sur Rust alors que la faille touche de nombreux langages. C'est juste que la faille a été découverte via Rust en premier.
Le 12/04/2024 à 20h25
Mais CreateProcess fonctionne différemment : Microsoft
On passe une ligne de commande - pas un exécutable et ses paramètres - , ce qui introduit tout un tas de problèmes :
- celui des espaces
- celui des caractères spéciaux, qui t'en qu'à faire sont différents de Windows à Linux
- ... une limite de 32767 caractères
Pour reprendre Java, et je suppose que Rust fait pareil, on utilise bien des tableaux dans le code du langage : GitHub
Côté Windows, on voit que Java recalcule une ligne de commande : GitHub
Côté Linux c'est (bien) différent : on passe des tableaux, que des tableaux, rien que des tableaux avec la seule contrainte du caractère NUL en fin de chaîne.
- GitHub
- GitHub
Et pour le coup, si Windows ne fournit pas l'API pour éviter (car il s'agit de ça au final) de prendre les paramètres et de recréer une ligne de commande, alors oui Windows est responsable de la situation.
(si les liens ci-dessus référencent Java 21, ça existe depuis au moins Java 8, donc ce problème Windows qui concerne Rust, Java, etc.... est là depuis au moins 2013 :))
Le 12/04/2024 à 21h25
Et les deux ont aussi des différences en dehors du caractère d'échappement. Il est beaucoup plus simple avec l'approche de Windows de vérifier la taille max des arguments de la ligne de commande (alors que sous linux, il faut la reconstituer, sans oublier de compter les espaces !). Car oui, il y a une taille max !
De même, l'approche utilisée par Windows préserve les espaces. S'il y a besoin de 3 espaces entre deux arguments, c'est possible de le faire sous Windows, pas sous Linux.
Encore une fois, il n'y a pas une approche qui soit meilleure qu'une autre. Ce sont juste 2 approches différentes.
Le 12/04/2024 à 23h30
Cela passe pour un shell, où l'utilisateur entre des commandes, mais pas pour des APIs bas niveau.
Et je ne vois pas le souci d'espaces dans la façon Linux : tu passes juste un tableau de chaîne de caractères (terminées par le caractère NUL). Le fait de vérifier la taille max de l'ensemble n'est pas plus coûteux : vu la différence de temps de lancement entre Linux et Windows (plus lent), ce n'est certainement pas ça le plus coûteux !
Si justement : dans le cas présent, il s'agit d’exécuter un programme depuis un autre programme avec les paramètres connus.
D'une part, la façon de faire de WIndows introduit des problèmes d'analyses vu que les règles sont difficiles à utiliser (je dis ça d'expérience de batch), pas documentée sur la doc de CreateProcess (pas trouvé sur la page de lien ou d’explication), d'autre part ça introduit des failles de sécurité du fait de cette complexité et de cas limites (notamment sur les caractères ").
Or, il s'agit bien de ça : Rust a une faille critique à cause de cette façon de faire qui nécessite un travail compliqué, mal compris et pour rien
Le 13/04/2024 à 00h59
La question est de savoir si les paramètres du ligne de commande sont nécessaires au système d'exploitation ou pas. La réponse est non. L'OS à juste besoin de savoir le programme à lancer, et les paramètres à passer, mais n'a pas à connaitre la structure des paramètres (tableau ou ligne).
Sous Linux, le choix a été fait d'organiser les paramètres sous forme de tableau. Sous Windows, c'est une chaine de caractère. Les deux sont tout à fait viable et ont de subtiles différences.
Je ne parlais de la complexité en terme de temps de calcul, mais de la complexité d'avoir un algorithme correcte. Et j'avoue ne pas trop voir en quoi le temps de lancement vient jouer un rôle ici. On parle des paramètres des programmes. Cela n'a strictement rien à voir.
Jusqu'au jour où on va se rendre compte que linux a en fait potentiellement le même problème, car quand un script est exécuté, l'interpréteur est déterminé via le shell bang. Chaque interpréteur peut avoir des règles d'échappements différents. Du coup, là aussi, on accusera Windows ?
Pas pour rien non. Quand les systèmes sont différents, il faut bien palier les différences. C'est le rôle du pattern adapteur ou d'un wrapper en programmation. Mais parfois c'est compliqué oui. Comme ici.
Le 13/04/2024 à 11h42
C'est le cas autant sous Windows que sous Linux.
D'ailleurs, j'ai aussi trouvé la doc qui explique ce que fait Windows pour transformer une chaîne en paramètres :
Microsoft
Justement : l'algorithme est simple.
Calculer la somme des tailles de chaîne d'un tableau c'est très facile.
Rien ne justifie la méthode de Windows, certainement pas la complexité algorithme ou le temps d’exécution de l'algorithme.
Sauf que les règles du shebang sont (probablement) entièrement liées au système : j'imagine que sous Linux, il commence déjà par voir à quoi il a affaire, programme ELF ou simple fichier avec shebang, puis il fait son travail : ce n'est pas l'application qui doit faire ce travail.
Si par exemple le caractères 0x36 doit être interdit, c'est le système que tu patches : pas chaque programme souhaitant lancer un programme.
Or, c'est justement ça le cœur du problème ici : Rust doit gérer ça, Java doit gérer ça, ...
Certes, mais tout le débat ici c'est justement que cette différence n'a aucun sens dans le cas présent et ne fait qu'introduire des failles de sécurité pour rien...
Si on veut parler d'un adapter/wrapper, probablement que tous les langages utilisant CreateProcess devraient passer par une API commune, cela réduirait les surfaces d'attaque.
Le 13/04/2024 à 11h50
Et ça me donne l'impression qu'au final, il refait un exeve en ayant juste changer le programme pour celui du shebang et ajouter le paramètre optionnel en début du tableau = pas de ligne de commandes.
Le 15/04/2024 à 16h41
Microsoft
Pas d'argc/argv...
Le 13/04/2024 à 07h51
Dans le premier cas les paramètre sont simplement passés comme arguments à l’exécutable, un simple échappement suffit, alors que dans le second cas les paramètre sont interprétés comme une ligne de commande ce qui n'est clairement pas ce qu'attendent les utilisateurs, et qui est difficilement échappable.
On pourrait au moins excuser ce comportement étrange s'il était documenté.
Le 13/04/2024 à 10h08
Cela n'en constitue pas pour autant une faille dans l'API Windows.
Modifié le 13/04/2024 à 10h38
Il n’empêche que, le fait que CreateProcess traite les arguments comme on traite une ligne de commande n'est ni le comportement auquel on s'attend, ni ce qui est documenté, et il en résulte une possibilité d'exploitation.
Dire si c'est une faille ou pas, c'est jouer sur les mots. En tout cas, ça en a les symptômes et ça devrait vraiment être corrigé, au minimum par une mise à jour de la documentation.
Le 15/04/2024 à 16h43
Modifié le 12/04/2024 à 20h48
Ce n'est pas le cas ici.
Le standard sur lequel s'appuie Rust est celui de GNU C, pas "Linux" (qui est un noyau). Rien de bien nouveau, et qui à défaut d'être une norme, est un standard d'interopérabilité.
Qui plus, un vieux standard : on ne parle pas de dernière fraîcheur.
La vieillerie
cmd.exe
contient une manière très particulière (comme personne d'autre) de fonctionner, et ce depuis donc bien trop longtemps.Une n-ème démonstration que l'habitude de M$ de produire du non-standard crée un/des risque(s) pour les utilisateurs de leurs produits.
Les langages doivent donc adopter un comportement particulier pour ce système d'exploitation particulier. D'aucuns dirait que c'est bien le système d'exploitation le problème.
Cf. https://blog.rust-lang.org/2024/04/09/cve-2024-24576.html
Le 12/04/2024 à 21h46
Pour être plus précis, le standard derrière s'appelle Posix (et pas GNU C). Sauf que l'API Windows existe 1985, POSIX depuis 1988. Windows ne respecte donc pas un standard qui n'existait tout simplement pas l'époque des premières versions du système d'exploitation.
C'est le principe de la bibliothèque standard de chaque langage. Quand tu as un langage dont l'API est proche de Posix, c'est plus simple à développer sur un système Posix que sur un système non Posix.
Le fait que plusieurs personnes se sont cassées les dents et qu'il y ait une faille à ce sujet aujourd'hui montre 2 choses :
- écrire une implémentation d'une API standard, ce n'est pas quelque chose de trivial
- un manque de documentation de l'API Windows.
Mais cela ne signifie pas que l'API présent une faille comme beaucoup le pense.
Le problème aujourd'hui touche uniquement Windows car :
- les systèmes à base de Linux (distribution classique, Android, etc.) respectent plus ou moins le standard POSIX
- les BSD respectent plus ou moins POSIX
- MacOS (basé sur BSD) respect plus ou moins POSIX
En fait, Windows est le seul système d'exploitation "grand publique" qui ne soit pas Posix de nos jours.
Comme dit plus haut, le standard n'existait pas à l'époque des premières versions de Windows.
Modifié le 12/04/2024 à 22h47
Oui Rust, Java & co doivent trouver une manière correcte pour contourner cette limitation de Windows. Mais pas seulement, c'est a peu près certain qu'on trouve dans le monde des dizaines de milliers de failles à cause de cette limitation problématique et difficilement compatible avec la manière dont la fonction main est spécifiée dans la plupart des langages (les argument sont une liste de string, pas une string)
Si, il y a une approche correcte, qui maintient la sémantique de la liste d'arguments sur toute la ligne, de la commande ou du script jusqu'à la fonction main du programme, et une approche fondamentalement problématique et qui encourage les failles de sécurité, mais qui marchotte à peu près avec des hacks et des contournements.
Le fait que les langages de programmation aient repris cette sémantique indique bien que tout le monde considère ça comme la manière correcte de faire. Je ne connais aucun langage qui ait choisi de prendre ses arguments comme une seule grande chaîne de caractères au lieu d'une liste.
Le 13/04/2024 à 00h19
Et une fois encore, ce n'est pas une limitation de Windows. C'est une problématique entre un environnement POSIX et un non POSIX. C'est radicalement différent.
Sérieusement ? Appeler ce qui relève d'un adapteur ou d'un wrapper un hack ? Sinon, certains langage n'avaient pas la notion de tableau d'arguments, mais bien d'une ligne : BASIC, Fortran à partir de 2003 (bon, qui propose les deux approchent :p), mais qui n'avait aucun mécanisme standard avant. Il me semble que COBOL permet les deux aussi. Et je suis loin de connaitre tous les langages.
Le problème est que tu considères la sémantique de la liste d'arguments comme allant de soi et étant une obligation aujourd'hui. Même si c'est très répandu, c'est loin d'être toujours le cas.
Ma correction.
Sinon, cette faille est du même acabit qu'une injection SQL qui serait exploitable car le connecteur à la base de données, sensé protéger via l'utilisation de requêtes paramétrées, fait mal son boulot. Est-ce qu'on dit pour autant que ce sont les SGBD qui sont troués ? Ou les API qui sont mal utilisées ?
Le 13/04/2024 à 10h48
Le comportement de windows est contre-intuitif et incohérent :
quand tu es dans ton programme, les arguments sont reçues sous forme de tableau de char
* quand tu appelles CreateProcess, les arguments doivent être fournis sous forme de ligne de commande
J’ai beaucoup de choses à reprocher au modèle fork/exec de posix, mais à minima il est cohérent dans la manière dont sont passés les arguments (qui date de bien avant C89, probablement avant C K&R, donc avant 73, bien avant windows).
Rajoute à ça que CreateProcess a un comportement différent sur les fichiers bat et les fichiers exe, et que si l’appelant ne précise pas l’extension il ne sait pas ce qui va être appelé, tu as une api qui est en réalité non prévisible.
Après, la responsabilité de la faille, c’est comme on veut. MS a la responsabilité d’avoir exposé une API pourrie (ce qui peut arriver), mais surtout de ne pas l’avoir remplacée par une API plus simple / prévisible depuis tout ce temps (c’est pas comme si l’api win32 était truffée de -Ex qui remplacent une api pourrie, conservée pour la compatibilité uniquement), ce qui est moins excusable.
Modifié le 13/04/2024 à 05h17
Le 15/04/2024 à 09h28
Le 15/04/2024 à 09h42
Le changement de noyau NT reflétait surtout une modification profonde de la manière de communiquer avec le matériel. De manière très simpliste (car c'est loin d'être le seul changement mais sans doute le plus significatif), avant NT, les drivers pouvaient accéder directement au matériel (en by-passant complètement le noyau donc), ce qui n'est plus le cas avec NT.
Winapi, première version, date du 20 novembre 1985 d'après Wikipédia et existait bien avant le noyau NT.
Il ne faut pas confondre le noyau (qui gère entre autre le matériel) des API exposés pour les programmes.
Le 15/04/2024 à 16h49
"To run a batch file, you must start the command interpreter; set lpApplicationName to cmd.exe and set lpCommandLine to the following arguments: /c plus the name of the batch file."
(doc de CreateProcess)
Je ne vois pas où est la partie qui dit que tu peux exécuter un bat... (ça parle d'exécuter un module)
Modifié le 13/04/2024 à 07h37
CreateProcess, quand il lance un exécutable (fichier exe) classique est correctement échappé. Le truc, c'est que si on lui fait exécuter un fichier de commandes windows (fichier bat ou cmd), il démarre en sous-main un interpréteur cmd.exe va traiter l'ensemble des paramètres comme une ligne dans un fichier de commande avec toutes les fonctionnalité associées beaucoup trop complexes pour un simple démarrage de script. Les règles d’échappement sont différentes, mais il y a aussi le fait que les variables d’environnement, mots clé et caractères spéciaux du langage batch sont traitées ...
Alors oui on peut dire que c'est un choix de fonctionnement de Windows et Rust doit s'adapter à la situation. C'est d'ailleurs ce que Rust a fait au final, là où Java a considéré que ce n'était pas son problème. Mais pour faire cela bien dès le début, encore aurait il fallu que ce comportement ait été correctement documenté. Or la documentation MSDN actuelle de CreateProcess ne dit rien de l'utilisation implicite de cmd.exe pour l'exécution des fichiers de commande, au contraire, elle propose de lancer cmd.exe manuellement.
Il est difficile de reprocher à Rust de ne pas avoir anticipé un comportement non documenté d'une API Windows dont les conséquence sur la sécurité avaient échappé a la plupart jusqu'à présent.
Le 12/04/2024 à 17h09
Modifié le 12/04/2024 à 17h51
Il ne faut jamais relâcher sa vigilance et surtout ne pas reposer sur les discours de ce type et les croyances. Même si le langage est conçu avec une gestion permettant d'éviter certaines erreurs, il y a toujours des risques.
Et si j'ai cette opinion, c'est parce qu'on entend encore de nos jours le poncif éculé et mensonger de "je suis sous Linux je suis invulnérable aux malwares" (même chose pour Mac, alors qu'il y a peu une faille des processeurs M1 a été dévoilée). La sécurité IT ne repose pas sur une marque mais un ensemble de pratiques. Le problème lorsqu'on s'engouffre dans ce genre de fantasme, c'est qu'on baisse son attention.
Modifié le 13/04/2024 à 06h12
Donc même s'il n'est pas a l'abri de tous les problèmes, Rust reste clairement plus porté sur la sécurité que la majorité des langages.
Le 13/04/2024 à 08h50
D'où :
Choisir un langage apportant plus de sécurités fait donc parti de cet ensemble de pratiques. Auquel il faut ajouter la veille autour, et de ce qui est satellite. D'où mon exemple caricatural de "Linux c'est invulnérable aux malwares". Se reposer sur le discours de sécurité de Rust (que je n'ai jamais remis pas en cause, j'en suis incapable) est un risque en soit de réduire sa vigilance.
Exemple caricatural : le programme est plus sûr grace aux capacités du langage, tant mieux. Mais si la DB est exposée aux 4 vents, la sécurité est zéro pointé.
D'où, à nouveau :
Ma crainte au sujet du discours continu "Rust = plus sécurisé", c'est justement d'engendrer l'effet inverse.
Modifié le 13/04/2024 à 12h34
Je suis d'autant moins convaincu que la philosophie de Rust contrairement à des langages comme Java, n'est pas d'automatiser les problématiques de sécurité pour les oublier, mais d'aider à les relever et à les prendre en charge, ce qui pousse au contraire à en prendre conscience. Rust m'a poussé a m’intéresser à la sécurité plutôt que l'inverse.
Je suis beaucoup plus conscient des problèmes de sécurité, y compris dans les autres langages, depuis que j'ai appris le Rust, et pas forcément que sur ce qui touche à la mémoire, car la communauté Rust ne tend pas vraiment à se reposer uniquement sur ce qu'offre le langage. Il y a notamment déjà pas mal d'outils qui permettent d'aller bien au delà des garanties mémoire du langage. De ce que j'ai pu constater, il y a beaucoup plus d'attention aux problématiques de sécurité en général dans les communautés Rust, que dans les communautés C et C++. C'est probablement pas pour rien que le problème a été identifié en Rust alors qu'il concerne bien d'autre langages dont certains depuis bien plus longtemps.
Le 12/04/2024 à 18h52
Le 12/04/2024 à 21h31
Les .bat .cmd, et la construction de lignes de commandes sous Windows est un enfer. Utilisant souvent des scripts pour lancer des processus, via une ligne de commande construite, j'ai souvent eu des problèmes avec les caractères d'échappement et l'encodage.
Ca m'a même value de remplacer un exe de office chargé de lancer le bon exe car IE encodait le caractère d'échappement: même Ms s'y perd...
Le 12/04/2024 à 20h34
C'est bien une faille Rust (ou autre langage) le portage n'a pas su adapter les spécificités de lancement des programmes et .bat de Windows.
Ça fait une mauvaise pub pour Rust mais Windows n'en sort pas grandit non plus...
Le 13/04/2024 à 09h53
Le 14/04/2024 à 14h52
Le 15/04/2024 à 07h35