Chapitre 2. La sécurité d'un système en général

Table des matières

Théorie: comment ca marche ?
Les mots de passe
Les privilèges des processus
L'utilisation d'une chroot
La sécurité de X
Vulnérabilités: à quel point c'est vulnérable ?
Les failles liées à la configuration
Création d'une backdoor avec un shell setuidbité
Casser la chroot
Le danger des RootKit (rk)
Exploitation d'un Xauthority
Défense: comment se protéger ?
Augmenter la sécurité d'un noyau Linux avec Grsecurity
N'autoriser que le minimum de connexions réseaux
Les IDS locaux et les anti-rootkits

Théorie: comment ca marche ?

Les mots de passe

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.

Exemple 2.1. Fichier /etc/shadow

root:RTE38gjCzIByc:11658:0:99999:7:-1:-1:1073867038

steck:$1$m9hSkswk$kTCQBu/CfFscJ3uMjXJZy/:13205:0:99999:7:::

Les privilèges des processus

A quoi servent ils ? Pourquoi est-ce dangereux ?

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 : 100
bash$ su; chown root a.out; chmod 4111 a.out; exit
bash$ ./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.

Comment éviter les problèmes de sécurité relatif aux droits ?

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.

Comment les utiliser ?

  • 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).

L'utilisation d'une chroot

man 2 chroot() et /usr/bin/chroot

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.

Problèmes liés à un environnement restreint

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.

Inconvéniant

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.

Exercice: chrooter un shell

A l'aide des scripts précédents, chrooter /bin/sh et executer le. Trouver une technique pour faire un listing d'un répertoire sans ls.

La sécurité de X

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.