Mail/M'écrire


Comment cacher des mots de passe dans un script ?
(André Brouty octobre 2006 - dernière mise à jour le 16 avril 2015)
Cet article est sous licence libre LLDD.

Introduction.

Les administrateurs système utilisent souvent des scripts de mise à jour de données lancés en batch. Le problème est que pour accéder aux données à lire ou modifier il faut parfois donner un mot de passe. C'est le cas si on doit mettre à jour des bases de données comme Mysql ou Ldap, et pour cela on met le mot de passe en clair dans le script, ce qui peut poser des problèmes de sécurité au cas où le mot de passe serait lu par hasard par quelqu'un qui n'a pas à le connaître.

Je me suis donc demandé s'il n'y avait pas un moyen pour un script de récupérer un mot de passe de manière sécurisé de telle sorte que quelqu'un, n'ayant pas les droits "root", mais ayant accès au script ne puisse pas voir le mot de passe utilisé par le script. Le but de cet article est de décrire et d'analyser une méthode permettant la protection de mots de passe utilisés par des scripts shell ou perl ou python ou php ou ...

Le principe de camouflage

Si on ne veut pas mettre les mots de passe en clair dans un script shell, il faut les mettre ailleurs et permettre au script de les récupérer. L'idée est donc de les mettre dans un programme binaire fait pour cela et de faire exécuter ce binaire par le script pour récupérer les mots de passe mais en s'arrangeant pour que le binaire ne délivre ces mots de passe qu'au script et à lui seul. Dans la suite de cet article on appellera ce binaire stockant les mots de passe donnemdp.

Il y a plusieurs façons de stocker un mot de passe dans un binaire, soit dans une chaine de caractères soit dans un tableau. La chaine de caractères est à déconseiller car elle peut être facilement récupérée par la commande strings passée sur le binaire. La méthode du tableau est meilleure de ce point de vue. Cependant il ne faut pas perdre de vue qu'un binaire peut être désassemblé et dans ce cas on peut récupérer les tableaux. La première précaution à prendre est d'interdire en lecture ce binaire mais pas en exécution (droits 111). C'est l'avantage du binaire sur le script, il n'est pas nécessaire d'être ouvert en lecture pour l'exécuter. Le script lui le doit car il faut lire les instructions qu'il contient pour les exécuter.

L'idée va donc être de placer chaque caractère du mot de passe dans les cases d'un tableau. Pour augmenter la sécurité et rendre plus difficile un éventuel désassemblage on prendra un tableau plus grand que la longueur du mot de passe (2 à 3 fois), on répartira les caractères du mot de passe aléatoirement dans le tableau et on remplira les cases vides par des caractères aléatoires de bourrage. Cela empèche de repérer les valeurs du mot de passe dans un éditeur de binaire et rend plus difficile le désassemblage. Rappelons-nous que ce binaire est interdit en lecture pour tous. Maintenant il faut s'assurer que ce binaire ne délivrera les mots de passe qu'au script qui l'appelle et à personne d'autre. Pour cela on va coder dans le binaire l'inode, les major/minor et l'empreinte md5 du script, ce qui lui permettra de vérifier que c'est bien le script autorisé qui l'appelle. Voici donc le principe de camouflage des mots de passe dans un binaire réservés à un script qui a besoin de les récupérer.

Sécurité de cette technique

Maintenant que l'on sait cacher des mots de passe dans un binaire pour ne les délivrer qu'à des scripts autorisés, examinons les problèmes de sécurité associés à cette technique. C'est-à-dire n'y a-t-il pas un moyen d'afficher les mots de passe qui ne doivent transiter que dans le script ? A ce stade de notre étude la réponse est oui. Voyons comment ainsi que les méthodes pour y remédier.

Les arguments de trace.

La plupart des scripts embarquent des méthodes de débogage qui se font par l'usage d'arguments spécifiques donnés au script. Par exemple pour le shell l'argument "-x" permet d'exécuter un script en affichant les valeurs des variables. Si on a donné à une variable la valeur du mot de passe celui-ci sera affiché sur l'écran et l'on n'aura rien caché du tout!

La parade va consister à controler que le script n'est pas appelé avec des arguments de trace. Ce qui est possible car les arguments du script figurent dans la structure /proc que lit donnemdp, et ainsi si une commande de trace est utilisée donnemdp ne délivrera pas les mots de passe qu'il cache. La contre-parade de l'attaquant va consister dans ce cas à utiliser un interprète shell modifié et recompilé qui utilise des arguments de trace différents. Il est en effet facile de recompiler un interprète shell et de l'installer sur son compte. Pour éviter ce détournement d'interprète de commandes, donnemdp doit conserver l'inode et l'empreinte md5 de l'interprète shell usuel, puis prendre le chemin de l'interprète utilisé à la première ligne du script, d'en récupérer l'inode, l'empreinte md5 et des les comparer avec celles qu'on lui a données à la compilation. On voit que la fabrication du binaire donnemdp se complique, mais ce n'est pas tout.

Les commandes de traces extérieures.

Il existe aussi des commandes de traces des programmes comme strace, ltrace ou truss qui permettent de tracer les appels systèmes et de visualiser les valeurs des variables du programme. Ce qui permet de faire apparaître le ou les mots de passe que l'on veut cacher.

L'efficacité de ces commandes de traces dépendent de l'écriture de script car elle ne s'applique que sur des programmes binaires. Par exemple sur le script suivant:

#
VAR="valeur"
echo "Bonjour programme de test"
exit
la commande strace ne s'applique pas car ce n'est pas un binaire, par contre la commande strace /bin/sh le_script fonctionnera parfaitement et donnera la valeur des variables. Si on écrit le même script comme suit:
#!/bin/sh
VAR="valeur"
echo "Bonjour programme de test"
exit
alors la commande strace le_script fonctionnera parfaitement car c'est le binaire /bin/sh qui est appelé sur le_script comme dans le cas précédent.

On voit donc que pour parer à cette attaque le binaire donnemdp devra se méfier de toute commande appelant le script, c'est-à-dire de toute commande ayant le nom du script dans ses arguments. C'est possible, il lui suffit de balayer la structure /proc et de vérifier les noms des arguments de tous les processus qui tournent sur la machine. Si on le trouve comme argument d'un processus en exécution les mots de passe ne seront pas délivrés.

On voit bien l'énorme contrainte de ce contrôle: il ne faut pas qu'un programme ouvre le script au moment de son exécution si on veut disposer des mots de passe. De plus il devient impossible d'embarquer le script dans un autre script ce que font souvent les administrateurs système.

On peut assouplir la situation en autorisant le binaire donnemdp à accepter un ou plusieurs scripts qui appellent le script appelant donnemdp. Mais il faut prendre des précautions de manière à ce l'on ne copie pas strace sur le nom de ce script ou même de faire un lien du nom du script sur strace. Pour cela il faut cabler l'inode et les major/minor du second script autorisé à appeller le premier de manière à éviter le détournement de ce script. Suivant l'usage de ces scripts il pourra aussi être utile de coder l'empreinte md5 des scripts pour éviter la copie de strace. Il ne faut pas oublier que le contrôle se fait sur la présence du script dans les arguments. Quelqu'un qui recompilerait strace pour mettre le nom du script dans le code ou le faire lire dans un fichier passerait outre le contrôle de donnemdp. La on n'a pas moyen de le vérifier. On voit bien que la protection du mot de passe ne peut pas être totale. Cependant la méthode que nous proposons peut être utile dans un contexte où les machines utilisant des scripts à mots de passe ne sont accessibles que par des administrateurs ou opérateurs soucieux de la sécurité et de la bonne gestion des systèmes.

Le problème des major/minor.

Pourquoi coder les major/minor du script ? Pour l'empécher d'être recopié sur un autre disque avec la même inode et le même nom. Dans le cas où le script est autorisé en lecture pour tous et en exécution que pour le propriétaire, alors le copier revient à autoriser l'exécution pour d'autres que le propriétaire. Cependant le codage des major/minor du script dans donnemdp peut conduire à des impasses. C'est le cas si on appelle donnemdp sur un partage de fichiers comme nfs ou samba où ce concept de major/minor n'a pas de sens. Un script qui appellerait donnemdp sur le disque physique alors qu'un autre l'appelerait d'une machine cliente distante qui le verrait sur un partage réseau n'obtiendrait pas le même major/minor d'où échec pour le second script. Le partage réseau pose donc un problème pour la fabrication de donnemdp.

L'implémentation.

À la lumière de l'analyse ci-dessus on voit que la fabrication du binaire donnemdp n'est pas très simple. Pour le créer on dispose donc d'un fichier de configuration qui permet de faire des choix d'implémentation pour l'administrateur système suivant ses besoins et le niveau de sécurité qu'il peut garantir.

On pourra en particulier avoir les possibilités suivantes:

  1. Choisir les arguments de debug à interdire au script appelant
  2. Ne pas contrôler les major/minor
  3. Ne pas contrôler l'usage de strace (Danger!)
  4. Donner le nom d'un script autorisé à appeler le script qui appelle donnemdp
  5. Cabler ou non les empreintes md5 de ces scripts pour éviter leur détournement
  6. Controler les noms et empreintes des interprètes shell qui appelleront le script
Par défaut on dispose de la sécurité maximale: les arguments de debug sont "-x", "-X", "-d", "-D"; contrôle des minor/major, controle de l'usage de strace, l'interprète autorisé est /bin/sh.

Les contraintes de cette implémentation.

Les contrôles faits par le programme se font en examinant la mémoire centrale à travers le pseudo système de fichiers /proc. Cela suppose que le système Unix utilisé implémente l'accès à /proc. C'est le cas pour les Linux de noyaux 2.4 et 2.6

L'usage de l'empreinte md5 impose de disposer des API md5. Ces API varient suivant les Linux. Ici nous utilisons les API md5 embarqués dans openssl. Ce qui suppose que les paquets openssl et openssl-devel soient installés. Ce programme a été mis au point et testé sur Linux Fedora et porté en 2010 sur Ubuntu. Sur ce dernier système c'est le paquet libssl-dev qui doit être installé.

REMARQUE IMPORTANTE: Ce programme est prévu pour être appelé par un script shell, mais il pourrait aussi être appelé par un programme binaire ayant besoin de récupérer des mots de passe. Ce n'est pas possible avec cette implémentation qui est prévue pour un shell lancé sur le script.

Usage du programme.

Ce programme peut stocker autant de mots de passe que l'on veut associés à des fichiers de script. Un même script peut stocker plusieurs mots de passe, il suffit de donner le même nom de fichier avec d'autres mots de passe et dans ce cas les mots de passe sont délivrés dans l'ordre de rentrée. Prenons des exemples de script:
#!/bin/sh
PASSWD=`./donnemdp`
./updateldap ldap.univ1.fr 389 $PASSWD
Sur cet exemple le script récupère un mot de passe, puis appelle un programme qui met à jour un serveur ldap sur le port 389 en lui passant ce mot de passe en argument.

#!/bin/sh
./updateldap ldap.univ1.fr 389 `./donnemdp 1`
./updateldap ldap.univ2.fr 389 `./donnemdp 2`
Dans ce second exemple le script a besoin de deux mots de passe pour mettre à jour deux serveurs ldap différents, et dans ce cas on donne le rang du mot de passe stocké dans le binaire.

Pour utiliser ce programme il suffit de lancer la commande install.sh et de répondre aux questions. Cette commande va compiler le programme pour y stocker les inodes, empreintes md5 et mots de passe, puis jeter le source. Si l'environnement de programation n'est pas installé sur la machine la compilation echouera.

Problèmes connus

On peut rencontrer des problèmes dans l'usage de donnemdp dont certains sont bien connus:
  1. utilisation des chemins en relatif. Dans le script on fait un appel à donnemdp pour obtenir le mot de passe. Cet appel peut se faire avec un chemin relatif ou un chemin absolu. Il est conseillé de le faire en absolu surtout si on utilise le mécanisme du cron. Si on le met en relatif on peut avoir un problème dans le cas où dans le script on fait un changement de répertoire avant l'appel à donnemdp, alors donnemdp ne sera pas trouvé. On peut avoir aussi un problème analogue si on appelle le script en relatif est que l'on fait un changement de répertoire dans le script car donnemdp fait un chdir pour se positionner à l'endroit où se trouve le script pour le lire et dans ce cas ne le trouvera pas. La solution consiste à appeler le script en absolu. Un problème du même type peut se poser avec les programmes autorisés à appeler le script et qui sont dans le fichier de configuration. Pour vérifier qu'ils ont le droit de lire le script donnemdp doit les lire ainsi que leurs arguments qui doit être le script. Si le script fait un chdir donnemdp risque de ne trouver ni l'un ni les autres. Il faut alors appeler le programme par son chemin absolu et le script en argument en absolu aussi.
  2. mauvais usage des arguments. Si on stocke plusieurs mots de passe dans donnemdp on peut les récupérer en donnant le numéro du mot de passe. Ces numéros sont obligatoirement positifs. Si dans un programme on appelle donnemdp dans une boucle avec un numéro commençant par zéro alors ce sont des mauvais mots de passe qui seront donnés.
  3. attention aux inclusions de scripts!! Dans certains langages comme perl ou python on a la possibilité d'inclure d'autres scripts dans le script principal avec require par exemple pour le langage perl ou import pour python. Dans ce cas il ne faut surtout pas mettre donnemdp dans un script inclus mais dans le script appelant sinon il est facile de modifier le script inclus sans problème puisque donnemdp ne contrôle que le script appelant. De même si on récupère le mot de passe dans une variable il ne faut pas passer cette variable à des fonctions figurant dans un script inclus qui n'est pas contrôlé. Pire il ne faut pas que la variable contenant le mot de passe soit globale car alors elle peut être affichée à partir des fichier inclus qui la connaissent.
    Cependant si dans un fichier inclus on ajoute la commande donnemdp quelque part et que l'on fait afficher le résultat alors on obtiendra le mot de passe. Pour éviter cela il faut valider l'option includecontrol dans le fichier de configuration et câbler tous les fichiers inclus dans donnemdp avec un mot de passe quelconque qui ne sera jamais utilisé. Ainsi si on modifie un fichier inclus les mots de passe ne seront pas distribués. L'inconvénient est qu'il faut refaire donnemdp dès qu'on modifie un fichier.

Téléchargement

Ce programme est disponible ici (version 2.2 avril 2015). Je l'ai testé sur de nombreux scripts, cependant je préfère le distribuer sous une version béta au cas où. Cette dernière version a été testée sur Linux Fedora 20, Ubuntu 12.04, Ubuntu 13.10, Debian testing .