CLI offensive : équivalence d’effets, variantes syntaxiques et détection
Deux commandes produisent le même résultat, mais pas la même trace textuelle. Une méthode de labo pour tester les règles CLI sans confondre variante et contournement.
Le 20 octobre, nous avons demandé à base64 de décoder T0sK de deux façons. --decode et --dec ont produit les mêmes trois octets : 4f 4b 0a, soit OK suivi d’un saut de ligne. Une recherche littérale de --decode ne verrait pourtant qu’une des deux lignes. Ce petit écart résume un problème bien plus intéressant que l’« obfuscation » spectaculaire : un programme interprète ses arguments selon son propre parseur, tandis qu’une règle peut n’observer qu’une chaîne de caractères. La valeur d’un test offensif tient à la mesure de cet écart, puis à la vérification de ce que le capteur a réellement enregistré.
L’idée de départ vient de la présentation de la présentation de Wietze Beukema à SecSea 2k24, You can run AND you can hide: Obfuscating Command Lines. Ses 37 diapositives montrent des syntaxes synonymes sur Windows, Linux et macOS. Nous lui devons cette piste ; les essais bénins ci-dessous ont été faits séparément sur notre hôte. Nous n’avons testé ni ses démonstrations Windows, ni une règle EDR, ni un contournement de produit.
Le programme et la règle ne lisent pas la même chose
Une ligne de commande traverse plusieurs interprètes. Le shell découpe et développe d’abord sa saisie ; le programme reçoit ensuite des arguments et leur attribue un sens. Enfin, un capteur enregistre une représentation de l’exécution, qui dépend du système, de sa configuration et du point de collecte. Égalité du résultat, égalité des arguments et égalité de la télémétrie sont trois propositions distinctes. C’est précisément ce que détaille la recherche de Beukema sur les arguments synonymes.
Le premier essai est assez petit pour qu’on puisse le contrôler à l’œil. Ici, od rend les octets visibles et évite de prendre deux sorties qui semblent identiques pour une preuve d’équivalence :
printf 'T0sK' | base64 --decode | od -An -tx1
printf 'T0sK' | base64 --dec | od -An -tx1
# Dans les deux cas sur notre hôte : 4f 4b 0a
Sur notre base64 GNU coreutils 9.4, l’abréviation longue --dec désigne sans ambiguïté --decode. Cette possibilité dépend du parseur et de ses options : l’ajout d’une autre option commençant pareil pourrait rendre une abréviation ambiguë. Elle ne devient pas une propriété de « Linux » en général, et une règle qui reconnaît déjà l’option abrégée n’a rien à contourner. Le manuel GNU Coreutils explique ce principe d’abréviation non ambiguë ; les slides l’appliquent à base64.
Le deuxième essai nous a forcés à regarder le programme précis plutôt que le nom de l’option. Nous avons soumis les mêmes octets hexadécimaux à trois écritures de xxd :
printf '4f4b0a' | xxd -p -r | od -An -tx1
printf '4f4b0a' | xxd -plain -revert | od -An -tx1
printf '4f4b0a' | xxd -password -reset | od -An -tx1
# Les trois sorties locales : 4f 4b 0a
Le binaire testé s’annonce comme xxd 2023-10-25. -password et -reset ont une apparence trompeuse : dans ce parseur, ces chaînes sont acceptées et aboutissent ici au même résultat que -p -r. Les diapositives de Beukema attirent l’attention sur cette famille de variantes ; elles ne dispensent pas de rejouer l’essai sur la version présente chez la cible. Nous ne déduisons pas du résultat que toutes les entrées, toutes les plateformes ou tous les builds de xxd se comportent de la même façon.
Documenter les variantes rejetées
Dans un laboratoire, nous partirions d’une commande de référence inoffensive, puis changerions un seul argument à la fois. Pour chaque variante : version du binaire, entrée, code de retour, sortie, erreurs et effets fichiers ou réseau s’il y en a. Un cas volontairement invalide compte aussi : il prouve que le banc d’essai sait reconnaître un échec au lieu de déclarer tout essai « équivalent ». Ensuite seulement, on confronte les exécutions aux événements collectés et au résultat d’une règle donnée. Le tableau utile comporte la ligne soumise, le sens exécuté, l’événement enregistré et la décision de la règle ; une capture de sortie terminal seule n’occupe que la deuxième case.
Si le capteur ne collecte pas les arguments, le test ne permet pas d’évaluer la signature. Si une alerte provient de l’arbre de processus ou d’un effet sur le disque, il faut la conserver dans les résultats, même si la règle textuelle n’a pas reconnu la commande. Une règle qui ne reconnaît que --decode présente une lacune limitée à la version et à la télémétrie testées.
Les exemples Windows de la présentation, notamment les séparateurs d’options de findstr, illustrent le même problème à un autre étage : chaque utilitaire choisit ce qu’il accepte. La syntaxe publiée par Microsoft pour findstr n’énumère pas toutes les variantes démontrées dans les slides. Sans replay sur un Windows actuel, nous les citons comme observations de l’auteur en 2024, pas comme comportement garanti aujourd’hui. Les diapositives abordent aussi des outils de dump mémoire ; reproduire ces charges dans un article de méthode n’apporterait rien à l’expérience bénigne ci-dessus.
Une détection tient compte aussi de l’effet
Une règle textuelle qui dépend d’une seule graphie d’option doit être testée contre les variantes acceptées par le binaire surveillé. Pour compléter cette détection, on peut corréler la commande avec l’identité du programme, son parent, ses fichiers ouverts et ses connexions. Beukema recommande également cette corrélation dans sa présentation.
Le dossier de test doit contenir les deux appels, leurs effets observés, les événements collectés et les alertes produites par chaque règle. Pour conclure à une évasion, il faut vérifier l’ensemble des détections actives dans cet environnement.
#rootzero
Une question sur votre exposition ?
Écrivez-nous. Une personne de l’équipe technique vous répond, sources à l’appui.