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:
- Choisir les arguments de debug à interdire au script appelant
- Ne pas contrôler les major/minor
- Ne pas contrôler l'usage de strace (Danger!)
- Donner le nom d'un script autorisé à appeler le script qui appelle donnemdp
- Cabler ou non les empreintes md5 de ces scripts pour éviter leur détournement
- 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:
- 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.
- 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.
- 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 .