Table des matières
La gestion des mots de passe a toujours été un point sensible des systèmes. Au début les systèmes stoquaient les mots de passe directement en clair dans un fichier, mais ce système a vite été abandonné au profit du stoquage d'une emprunte du mot de passe. Dans la plupart des Unix, le fichier qui gère les utilisateurs est /etc/passwd. Mais ce fichier doit être lisible par tout le monde par il contient la correspondance uid/login. Les mots de passe sont donc de nos jours stoqués dans un autre fichier, souvent /etc/shadow (il y a dans ce cas une astérisque a la place dans le fichier passwd). Avant, ce qui était utilisé pour l'émprunte du mot de passe était un DES modifié (les S-tables ont été changées pour éviter les puces de crackage de DES existantes) : les huit premiers octets du mot de passe servaient de clé pour chiffrer un bloc de huit zeros (64 bits), en répétant l'opération 25 fois. Le résultat est stoqué sous forme d'une chaine de 11 caractères affichables (./0-9A-Za-z), dans laquelle un caractère représente 6 bits du résultat. Afin de se prévenir contre les dictionnaires de mots de passes déjà chiffrés, on utilise la technique du sel. Un nombre de 12 bits est choisi en fonction de time() et sert a faire des permutations sur le résultat et donc de multiplier par 4096 la taille des éventuels dictionnaires. Les deux caractères de sel sont stoqués juste avant le mot de passe encrypté.
Le principal inconvénient du DES est que seuls les huit premiers caractères du mot de passe sont utilisés, et donc qu'il est facile de le cracker. De nos jours, c'est une variante de MD5 qui est employée, qui se sert de huit caractères de sel pour produire un mot de passe encodé de 22 caractères en utilisant la totalité du mot de passe.
D'autres systèmes (OpenBSD par exemple) utilisent d'autres algorithmes d'encryption, comme BlowFish.
Un utilisateur est associé à un id personnel : UID, et à un id de group : GID. Dans le context d'un processus, c'est un peu différent, afin de faciliter le changement d'identité, il y a les RUID/RGID, pour les valeurs réelles (initialles), et les EUID/EGID pour les valeurs effectives. Le contrôle sur les accès d'un processus sont effectués à l'aide des valeurs effectives. A l'éxécution, par défaut tous les IDs correspondent aux IDs de l'utilisateur. Si le programme éxécuté est setuidbité, ou setguidbité (mode 04000 / 02000), alors la valeur effective devient celle du propriétaire du fichier.
Exemple 2.2. [ER][UG]D demonstration
int main() {
printf("EUID : %d\tRUID : %d\n", geteuid(), getuid());
printf("EGID : %d\tRGID : %d\n", getegid(), getgid());
}
bash$./a.out EUID : 1000 RUID : 1000 EGID : 100 RGID : 100bash$su; chown root a.out; chmod 4111 a.out; exitbash$./a.out EUID : 0 RUID : 1000 EGID : 100 RGID : 100
Dans un système POSIX, ce mécanisme est indispensable. Par exemple, pour qu'un utilisateur puisse changer son mot de passe, il faut bien qu'il puisse modifier le fichier contenant les mots de passes (/etc/shadow). Pour cela, il existe un programme setuid root : passwd, qui permet à l'utilisateur de bénéficier des droits de root le temps de l'éxécution de passwd.
Donc si un programme appartient à root et qui à les droits d'exécution pour tout le monde est exécuter sous l'identité de l'utilisateur qui le lance (real uid) mais si ce programme est set-uid il est alors lancer sous l'identité de root (effective uid). Ceci est dangereux car s'il existe une faille dans le programme qui permet a l'utilisateur d'exécuter la fonction qu'il souhaite, cette fonction sera exécutée dans le context de root, l'utilisateur aura donc accès a toutes les ressources de la machine.
Pour éviter des problèmes il existe une protection simple qui consiste a changer les droits du programme quand on en a besoin uniquement. Le programme set-uid est alors exécuté avec les droits de l'utilisateur normal (en fait on met la valeur de l'effective uid a celle du real uid) puis les privilèges sont augmentes (on remet la valeur initiale du effective uid) quand on a besoin de droits plus importants, on peut aussi exécuter le programme avec des droits élevés et descendre les privilèges au moment d'opérations dangereuses mais ceci est déconseillé. Ceci permet d'éviter la majorité des races conditions (conditions de concurrence : attaque qui joue sur le temps d'accès au fichier) et l'exécution de commande externe.
Activer les droits set-uid / set-gid:
Pour mettre le bit set-uid ou set-gid sur un programme il faut rajouter respectivement les modes 4000 (chmod 4xxx / chmod +s) et 2000 (chmod 2xxx / chmod +S) au programme.
Éviter les failles dans un programme en C.
Pour modifier temporairement l'UID effectif qui est celui utilisé pendant l'exécution d'un programme. on utilise l'appel système seteuid. L'ancien uid effectif est alors sauvegardé dans un champ nomme SUID (saved uid) on peut ainsi changer facilement entre le real uid (uid de celui qui lance le programme) et l'uid du programme. Lorsque le programme est suid root il change d'euid autant qu'il le veut c'est ce que fait le programme su. Exemple un programme qui monte une clef usb, copie un fichier a sa racine et la démonte. Ce programme a besoin de droits root pour mount et umount la clef mais doit garder les droits de l'utilisateur pour copier le fichier, c'est pour ça qu'il doit avoir le bit setuid active.
Exemple 2.3. setuid demonstration
uid_t euid_initial, r_uid;
int main(int argc, char **argv)
{
start_euid = geteuid();
/* sauvegarde l'euid initial dans le cas d'un programme set-uid c'est
l'uid de celui a qu'il appartient, ici root*/
ruid = getuid();
/* sauvegarde le ruid initial dans le cas d'un programme set-uid c'est
l'uid de celui qui la executer*/
seteuid(r_uid);
/* on set la valeur de l'euid a celle du real uid
(car les droits du programme sont ceux de l'euid)*/
mount_clef();
cp_file(argv);
umount_clef();
}
void cp_file(argv)
{
execl("cp", argv[0], "/mnt/sda1/", 0);
/* ici l'euid est egal au ruid pour pas que l'utilisateur puissent copier des
fichier qui ne lui appartiennent pas */
}
void mount_clef()
{
seteuid(start_euid);
/* on set l'euid a sa valeur initial, ici elle vaux 0 pour root,
pour pouvoir monter la clef */
mount("/dev/sda1", "/mnt/sda1", "vfat" , 0, 0);
/* on appelle la fonction mount en tant que root*/
seteuid(ruid);
/* on set a nouveau l'euid a la valeur du ruid pour pas que le programme
continue a s'executer avec les droits root*/
}
void umount_clef()
{
seteuid(start_euid)
umount("/mnt/sda1");
seteuid(ruid)
}
(Mal)heureusement ces techniques ne permettent de se proteger contre les buffer overflow car ces attaques permet l'execution de n'importe quelle commande on peut donc executer les commandes setreuid ou setregid pour récuperer les droits voulus (c'est un privilege-escalation).
Chroot est un appel système permettant de faire tourner un programme dans un environnement restreint. La commande chroot prend deux paramètres : le chemin du nouveau répertoire racine (/), et le chemin du programme à lancer (relatif au premier argument).
Ce mécanisme est idéal pour faire tourner un service web par exemple, on peut ainsi s'assurer que les clients n'auront pas acces au reste du système de fichier.
Il existe deux techniques différentes pour utiliser une chroot. La première consiste à exécuter l'appel système chroot juste aprés l'initialisation d'un service. Si le programme n'offre pas cette fonctionnalité, il faut utiliser la commande chroot, qui va d'abord faire l'appel système chroot, puis éxécuter le programme.
C'est beaucoup plus propre quand c'est le processus qui se chroot lui-même, car comme ca, dans le repertoire de chroot, il n'y a que ce dont il a besoin d'avoir access (fichier .html pour un serveur web, par exemple). Par exemple pour apache, il y a un module (mod_chroot) pour faire ca.
Le premier problème est lié aux librairies partagées. Lors de l'appel système exec sur un executable dynamique (pas compilé en -static), le 'runtime dynamic linker' est chargé de mapper en mémoire les librairies dynamiques du programme. La résolution de ce problème est trivial, il suffit de parser la sortie du programme 'ldd'. Voici un petit script qui permet de copier un binaire et ses librairies dans une chroot :
Exemple 2.4. ELF chrooter
#!/bin/bash
#
# chroot-obj.sh
#
if [ ! -n "$2" ]; then echo "usage: $0 elf chroot-dir"; exit -1; fi;
if [ ! -e "$1" ]; then echo "$0: error ! $1 is not a binary"; exit -1; fi;
if [ ! -d "$2" ]; then echo "$0: error ! $2 is not a directory"; exit -1; fi;
FILE=$1
CHROOT=$2
# Copie d'un fichier si son équivalent n'existe pas dans $CHROOT
function cp4ch {
if [ ! -f ${CHROOT}$1 ]; then
echo "-> $1 (${CHROOT}$1)";
cp --parents -p $1 $CHROOT || exit -1;
fi;
}
# Fonction utilisée pour copier un programme et ces librairies dans $CHROOT
function chroot-obj {
local j
cp4ch $1
for j in `ldd $1 | awk -F"=>" {'print $2'} | awk -F" " {'print $1'} | grep -v '^('`; do
cp4ch $j
done
for j in `ldd $1 | awk -F" " {'print $1'} | grep "^/"`; do
cp4ch $j
done
}
chroot-obj $FILE
Aprés avoir copié tout cela, l'appel system exec devrait bien se dérouler. Le second problème est lié au fonctionnement du programme, qui va avoir besoin de certains devices, (/dev/{null,zero,random,urandom}), certains fichiers de conf tel que : /etc/{localtime,ld.so.conf,ld.so.cache,nsswitch.conf} pour un programme standart, /etc/{resolv.conf,hosts} si c'est un programme réseau... Voici un autre petit script pour initialiser une chroot:
Exemple 2.5. Chroot init
#!/bin/bash
#
# chroot-obj.sh
#
if [ ! -n "$1" ]; then echo "usage: $0 chroot-dir"; exit -1; fi;
CHROOT=$1
# Copie d'un fichier si son équivalent n'existe pas dans $CHROOT
function cp4ch {
if [ ! -f ${CHROOT}$1 ]; then
echo "-> $1 (${CHROOT}$1)";
cp --parents -p $1 $CHROOT || exit -1;
fi;
}
mkdir -pv $CHROOT/{dev,etc,home,root,var} || exit -1
i=$CHROOT/dev/random
if [ ! -c $i ]; then echo "-> $i"; mknod -m 644 $i c 1 8 || exit -1; fi
i=$CHROOT/dev/urandom
if [ ! -c $i ]; then echo "-> $i"; mknod -m 644 $i c 1 9 || exit -1; fi
i=$CHROOT/dev/null
if [ ! -c $i ]; then echo "-> $i"; mknod -m 644 $i c 1 3 || exit -1; fi
i=$CHROOT/dev/zero
if [ ! -c $i ]; then echo "-> $i"; mknod -m 644 $i c 1 5 || exit -1; fi
for i in /etc/{localtime,ld.so.conf,hosts,ld.so.cache,nsswitch.conf,hosts,resolv.conf}; do cp4ch $i || exit -1; done
Enfin il reste les imprévus, c'est à dire d'autres fichiers de conf, ou les libs chargées dynamiquement par le programme. Pour ca, il n'y a pas de remêde miracle, il faut tracer l'execution du programme, avec l'outil strace, pour choper tous les appels système retournant ENOENT.
Ces techniques sont relativements expérimentales, tous les cas ne sont pas gérés, il se peut que le programme, soit instable. De plus cela n'est pas vraiment pas pratique lors d'une mise à jour du système.
Malheureusement, une chroot n'est qu'une restriction au niveau du filesystem, les acces au réseau et aux appels systèmes ne sont pas spécialement restreints. Par défault une chroot de linux ne fera que ralentir un attaquant, il existe plusieurs techniques pour sortir d'une chroot (appelées chroot breaking) qui marchent sur les noyaux les plus récents.
X, l'interface graphique d'unix, fonctionne en mode client/serveur. Pour qu'une appliquation (un client) puisse afficher une fenetre sur votre écran (le serveur), il faut :
soit connaîte le contenu du .Xauthority (cookie d'authentification). Par défaut, la libX va le chercher dans votre home, mais on peut le spécifier en variable d'environnement.
soit que le programme soit éxécuté sur une machine trustée par le serveur. Cela est vérifié à l'aide de l'ip de la machine. Le programme xhost permet de modifier la liste d'acces au serveur.
Après que la connexion soit initialisée, le client a access à tous les autres clients. C'est à dire qu'il peut catcher les évênements (entrée clavier), prendre un screenshot de l'ecran, ou même controler l'interface (en connectant un serveur vnc au display attaqué). Si le fichier .Xauthority est en lecture par tout le monde, la session X peut être volée, avec tout ce que cela implique.