Programmation avancée sous Linux


précédentsommairesuivant

10. Sécurité

Une grande partie de la puissance d'un système GNU/Linux vient de sa capacité à gérer plusieurs utilisateurs et de son bon support pour le réseau. Beaucoup de gens peuvent utiliser le système en même temps et se connecter au système à distance. Malheureusement, cette puissance présente des risques, en particulier pour les système connectés à Internet. Dans certaines circonstances, un « hacker » distant peut se connecter au système et lire, modifier ou supprimer des fichiers stockés sur la machine. Ou bien, un utilisateur pourrait lire, modifier ou supprimer les fichier d'un autre sans y être autorisé. Lorsque ce type d'événement se produit, la sécurité du système est dite compromise.

Le noyau Linux fournit divers mécanismes afin de s'assurer que de telles situations ne se présentent pas. Mais les applications classiques doivent elles aussi être attentives à éviter les failles de sécurité. Par exemple, imaginons que vous développiez un logiciel de comptabilité. Bien que vous puissiez vouloir permettre à tous les utilisateurs de remplir des notes de frais, il n'est certainement pas souhaitables qu'ils puissent tous les valider. De même, les utilisateurs ne doivent pouvoir consulter que leur propre bulletin de paie et les managers ne connaître que les salaires des employés de leur département.

Pour forcer ce type de contrôle, vous devez être très attentif. Il est étonnamment facile de faire une erreur permettant à des utilisateurs de faire quelque chose qu'ils ne devraient pas pouvoir faire. La meilleur approche est de demander de l'aide à des experts en sécurité. Toutefois, tout développeur d'application doit maîtriser quelques bases.

10-1. Utilisateurs et Groupes

Chaque utilisateur sous Linux est identifié par un numéro unique appelé user ID, ou UID. Bien sûr, lorsque vous vous connectez, vous utilisez un nom d'utilisateur plutôt que cet UID. Le système effectue la conversion entre ce nom d'utilisateur et l'UID, et à partir de ce moment, seul l'user ID est pris en compte.

Vous pouvez en fait faire correspondre plus d'un nom d'utilisateur au même UID. En ce qui concerne le système, seuls les UID comptent. Il n'y a aucune façon de donner plus de droits à un utilisateur qu'à un autre s'ils ont tous deux le même UID.

Vous pouvez contrôler l'accès à un fichier ou à toute autre ressource en l'associant à un UID particulier. Dans ce cas, seul l'utilisateur disposant de cet UID peut accéder à la ressource. Par exemple, vous pouvez créer un fichier que vous serez le seul à pouvoir lire ou un répertoire où vous seul pourrez créer de nouveau fichier. Cela suffit dans la plupart des cas simples.

Parfois, cependant, vous avez besoin de partager une ressource entre plusieurs utilisateurs. Par exemple, si vous êtes un manager, vous pourriez vouloir créer un fichier que tous les managers pourraient lire mais pas les employés ordinaires. Linux ne vous permet pas d'associer plusieurs UID au même fichier, vous ne pouvez donc pas créer une liste de tous les gens auxquels vous souhaitez donner accès au fichier et les lier au fichier.

Par contre, vous pouvez créer un groupe. À chaque groupe est associé un numéro unique, appelé group ID ou GID. Chaque groupe contient un ou plusieurs user ID. Un même UID peut faire partie de plusieurs groupes mais un groupe ne peut pas contenir d'autres groupes; ils ne peuvent contenir que des utilisateurs. Comme les utilisateurs, les groupes ont des noms. Tout comme pour les noms d'utilisateurs, le nom d'un groupe n'a pas d'importance, le système utilise toujours le GID en interne.

Pour continuer avec notre exemple, vous pourriez créer un groupe managers et y placer les UID de tous les managers. Vous pourriez alors créer un fichier qui peut être lu par n'importe qui dans ce groupe mais pas par les gens qui y sont extérieurs. En général, vous ne pouvez associer qu'un seul groupe à une ressource. Il n'y a aucun moyen de spécifier que les utilisateurs peuvent accéder à un fichier s'ils font partie du groupe~7 ou du groupe~42, par exemple.

Si vous êtes curieux de connaître votre UID et les groupes auxquels vous appartenez, vous pouvez utiliser la commande id. Voici un exemple de la sortie de cette commande:

 
Sélectionnez

% id
uid=501(mitchell) gid=501(mitchell) groups=501(mitchell),503(csl)

la première partie indique que l'UID de l'utilisateur ayant invoqué la commande est~501. La commande détermine le nom d'utilisateur correspondant et l'affiche entre parenthèses. On voit ici que l'utilisateur 501 est dans deux groupes: le groupe~501 (appelé mitchell) et le groupe~503 (appelé csl). Vous vous demandez certainement pourquoi le groupe 501 apparaît deux fois: une première fois dans le champ gid et une seconde dans le champ groups. Nous y reviendrons plus tard.

10-1-1. Le superutilisateur

Un des comptes utilisateur est très spécial(Le fait qu'il n'y ait qu'un seul utilisateur spécial est à l'origine du nom d'UNIX donné par AT&T à son système d'exploitation. Un système d'exploitation plus ancien qui disposait de plusieurs utilisateurs spéciaux a été baptisé MULTICS. GNU/Linux, bien sûr, est compatible avec UNIX. ). Cet utilisateur a l'UID~0 est a généralement le nom d'utilisateur root. On y fait parfois référence en parlant du compte superutilisateur. L'utilisateur root peut littéralement tout faire: lire ou supprimer n'importe quel fichier, ajouter de nouveaux utilisateurs, désactiver l'accès réseau, etc. Beaucoup d'opérations spéciales ne peuvent être réalisées que par des processus s'exécutant avec les privilèges root ? c'est-à-dire s'exécutant sous le compte utilisateur root.

Le problème de cette conception est qu'un nombre important de programmes doivent être exécutés par root car beaucoup de programmes ont besoin d'accéder à ces opérations spéciales. Si l'un de ses programmes ne se comporte pas correctement, les conséquences peuvent être dramatique. Il n'y a aucun moyen de contenir un programme lorsqu'il est exécuté par root; il peut faire n'importe quoi. Les programmes lancés par root doivent être écrit avec beaucoup de précautions.

10-2. User et Group ID de Processus

Jusqu'à maintenant, nous n'avons parlé que des commandes exécutées par un utilisateur en particulier. Cela ne colle pas exactement à la réalité car l'ordinateur ne sait jamais réellement quel utilisateur l'exécute. Si Ève découvre le nom d'utilisateur et le mot de passe d'Alice, Ève peut se connecter en tant qu'Alice et le système laisserait faire à Ève tout ce qu'Alice peut faire. Il ne connaît que l'UID en cours et non pas l'utilisateur qui saisit les commandes. Si on ne peut pas faire confiance à Alice pour garder secret son mot de passe, alors rien de ce que vous pourrez faire en tant que programmeur d'application ne pourra empêcher Ève d'accéder aux fichiers d'Alice. La responsabilité de la sécurité du système est partagée entre le développeur, les utilisateurs du système et les administrateurs du système.

À chaque processus est associé un UID et un GID. Lorsque vous invoquez une commande, elle lance un processus dont l'UID et le GID sont les vôtres. Lorsque nous disons qu'un utilisateur effectue une opération quelconque, nous voulons dire en réalité qu'un processus avec l'UID correspondant effectue cette opération. Lorsque le processus exécute un appel système, le noyau décide s'il y est autorisé. Pour cela, il examine les permissions associées aux ressources auxquelles le processus tente d'accéder et vérifie l'UID et le GID associés au processus tentant d'exécuter l'appel.

Désormais, vous savez ce que signifie le champ gid de la sortie de la commande id. Il indique le GID du processus courant. Même si l'utilisateur~501 fait partie de plusieurs groupes, le processus courant ne peut avoir qu'un seul GID. Dans l'exemple présenté précédemment, le GID en cours est~501.

Si vous devez manipuler des UID ou des GID dans votre programme (et vous aurez à le faire si vous écrivez des programmes concernant la sécurité), vous devez utiliser les types uid_t et gid_t définis dans l'en-tête <sys/types.h> ? même si les UID et GID ne sont en fait que des entiers, évitez de faire des suppositions sur le nombre de bits utilisé dans ces types ou d'effectuer des opérations arithmétiques en les utilisant. Traitez les comme un moyen obscur de manipuler les identifiants de groupe et d'utilisateur.

Pour récupérer l'UID et le GID du processus courant, vous pouvez utiliser les fonctions geteuid et getegid définies dans <unistd.h>. Ces fonctions ne prennent aucun paramètre et n'échouent jamais, il n'y a pas d'erreur à contrôler. Le Listing simpleid présente un simple programme qui fournit un sous-ensemble des fonctionnalités de la commande id:

Affiche les Identifiants d'Utilisateur et de Groupe simpleid.c
Sélectionnez

#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>
int main()
{
  uid_t uid = geteuid ();
  gid_t gid = getegid ();
  printf ("uid=%d gid=%d\n", (int) uid, (int) gid);
  return 0;
}

Lorsque ce programme est exécuté (par le même utilisateur que celui qui a lancé le programme id dans l'exemple précédemment), la sortie est la suivante :

 
Sélectionnez

% ./simpleid
uid=501 gid=501

10-3. Permissions du Système de Fichiers

Un bon moyen de voir les utilisateurs et les groupes en actions est d'étudier les permissions du système de fichiers. En examinant la façon dont le système associe les permissions avec chaque fichier et en observant comment le noyau vérifie qui est autorisé à accéder à quels fichiers, le concept d'identifiant utilisateur et de groupe devrait devenir clair.

Chaque fichier a exactement un utilisateur propriétaire et un groupe propriétaire. Lorsque vous créez un nouveau fichier, il est détenu par l'utilisateur et le groupe du processus créateur(En fait, il existe de rares exceptions faisant intervenir les sticky bits, décrits plus loin dans la Section 10.3.2, « Sticky Bits ».).

Les opérations de base sur les fichiers, en ce qui concerne Linux, sont la lecture, l'écriture et l'exécution (Notez que la création et la suppression ne sont pas considérées comme des opérations sur le fichier, elles sont considérées comme des opérations sur le répertoire contenant le fichier. Nous en parlerons plus loin). Si vous ne pouvez

pas lire un fichier, Linux ne vous laissera pas en examiner le contenu. Si vous ne pouvez pas y écrire, vous ne pouvez pas modifier son contenu. Si vous ne disposez pas des droits d'exécution sur un fichier contenant le code d'un programme, vous ne pouvez pas exécuter ce programme.

Linux vous permet d'indiquer lesquelles de ces trois actions ? lire, écrire et exécuter ? peuvent être réalisées par l'utilisateur propriétaire, le groupe propriétaire et toute autre personne. Par exemple, vous pouvez dire que l'utilisateur propriétaire aura tous les droits, que les membres du groupe propriétaire pourront lire et exécuter le fichier (mais pas y écrire) et que personne d'autre n'y a accès.

Vous pouvez consulter ces bits de permission de façon interactive avec la commande ls en utilisant les options -l ou -o et par programmation via l'appel système stat. Vous pouvez définir de façon interactive les bits de permission avec le programme chmod(On fait parfois référence aux bits de permission en utilisant le terme mode. Le nom de la commande chmod est un diminutif pour « change mode ».) et par programmation avec l'appel système du même nom. Pour examiner les permissions d'un fichier appelé hello, utilisez ls -l hello. Voici un exemple de sortie:

 
Sélectionnez

% ls -l hello
-rwxr-x---    1 samuel csl 11734 Jan 22 16:29 hello

Les indications samuel et csl signifient que l'utilisateur propriétaire est samuel et que le groupe propriétaire est csl.

La chaîne de caractères a début de la ligne indique les permissions associées au fichier. Le premier tiret indique que le fichier est un fichier classique. Il serait remplacé par d pour un répertoire ou d'autres lettres dans le cas de fichiers spéciaux comme les périphériques (reportez-vous au Chapitre peripheriques, « Périphériques ») ou les canaux nommés (voyez le Chapitre IPC, « Communication Interprocessus », Section tubes, « Tubes »). Les trois caractères suivants représentent les permissions de l'utilisateur propriétaire: ils indiquent que samuel peut lire, écrire et exécuter le fichier. Les trois caractères d'après donnent les permissions des membres du groupe csl; ses membres ne peuvent que lire et exécuter le fichier. Enfin, les trois derniers caractères indiquent les permissions de toute autre personne; ici, tout utilisateur n'étant pas samuel et ne faisant pas partie du groupe csl ne peut rien faire avec le fichier hello.

Voyons comment cela fonctionne. Tout d'abord, essayons d'accéder au fichier en tant qu'utilisateur nobody, qui ne fait pas partie du groupe csl:

 
Sélectionnez

% id
uid=99(nobody) gid=99(nobody) groups=99(nobody)
% cat hello
cat: hello: Permission denied
% echo hi > hello
sh: ./hello: Permission denied
% ./hello
sh: ./hello: Permission denied

Nous n'avons pas le droit de lire le fichier, c'est pourquoi cat échoue; nous ne pouvons pas écrire dans le fichier, c'est pourquoi echo échoue; et nous n'avons pas le droit d'exécuterle fichier, c'est pourquoi ./hello échoue.

Les choses s'arrangent si nous accédons au fichier en tant que mitchell, qui n'est pas membre du groupe csl:

 
Sélectionnez

% id
uid=501(mitchell) gid=501(mitchell) groups=501(mitchell),503(csl)
% cat hello
#!/bin/bash
echo "Hello, world."
% ./hello
Hello, world.
% echo hi > hello
bash: ./hello: Permission denied

Nous pouvons afficher le contenu du fichier et nous pouvons l'exécuter (il s'agit d'un simple script shell) mais nous ne pouvons toujours pas y écrire.

Si nous sommes identifiés en tant que propriétaire (samuel), nous pouvons même écraser le fichier:

 
Sélectionnez

% id
uid=502(samuel) gid=502(samuel) groups=502(samuel),503(csl)
% echo hi > hello
% cat hello
hi

Vous pouvez modifier les permissions associées avec un fichier si vous êtes son propriétaire (ou le superutilisateur). Par exemple, si vous voulez désormais permettre à tout le monde d'exécuter le fichier, vous pouvez faire ce qui suit:

 
Sélectionnez

% chmod o+x hello
% ls -l hello
-rwxr-x--x    1 samuel csl 3 Jan 22 16:38 hello

Notez qu'il y a maintenant un x à la fin de la première chaîne de caractères. L'option o+x signifie que vous voulez donner la permission d'exécution à tous les autres gens (ni le propriétaire, ni les membres du groupe propriétaire). Pour révoquer les permissions en écriture du groupe, vous utiliseriez g-w. Consultez la page de manuel de la section~1 sur chmod pour plus de détails sur sa syntaxe:

 
Sélectionnez

% man 1 chmod

Dans un programme, vous pouvez utiliser l'appel système stat pour déterminer les permissions associées à un fichier. Cette fonction prend deux paramètres: le nom du fichier sur lequel vous voulez des renseignements et l'adresse d'une structure de données renseignée avec des informations sur le fichier. Consultez l'Appendice B, « E/S de Bas Niveau », Section B.2, « stat », pour une présentation des autres informations que vous pouvez obtenir via stat. Le Listing statperm montre un exemple d'utilisation de stat pour déterminer les permissions associées à un fichier.

Déterminer si le Propriétaire a les Droits d'Écriture stat-perm.c
Sélectionnez

#include <stdio.h>
#include <sys/stat.h>

int main (int argc, char* argv[])
{
  const char* const filename = argv[1];
  struct stat buf;

  /* Récupère les informations sur le fichier. */
  stat (filename, &amp;buf);
  /* Affiche un message si les permissions sont définies de façons 
     à ce que le propriétaire puisse y écrire. */
  if (buf.st_mode &amp; S_IWUSR)
    printf ("Le propriétaire peut écrire dans '%s'.\n", filename);
  return 0;
}

Si vous exécutez ce programme sur notre script hello, il indique:

 
Sélectionnez

% ./stat-perm hello
Le propriétaire peut écrire dans 'hello'.

La constante S_IWUSR correspond à la permission en écriture pour le propriétaire. Il y a une constante pour chaque bit. Par exemple, S_IRGRP correspond à la permission en lecture pour le groupe propriétaire et S_IXOTH à la permission en exécution pour les utilisateurs qui ne sont ni propriétaires, ni membre du groupe. Si vous stockez les permissions dans une variable, utilisez le typedef mode_t. Comme la plupart des appels système, stat renverra~-1 et positionnera errno s'il ne peut pas obtenir les informations sur le fichier.

Vous pouvez utiliser la fonction chmod pour modifier les bits de permission d'un fichier existant. Appelez chmod avec le nom du fichier dont vous voulez changer les permission et les bits à activer sous forme d'un OU binaire entre les différentes constantes citées précédemment. Par exemple, l'extrait suivant rend hello lisible et exécutable par le propriétaire mais désactive toutes les autres permissions associées à hello:

 
Sélectionnez

chmod ("hello", S_IRUSR | S_IXUSR);

Le même système de bits de permission s'applique aux répertoires mais ces bits ont des significations différentes. Si un utilisateur est autorisé à lire le répertoire, alors il peut consulter la liste des fichiers présents dans ce répertoire. Avec les droits en écriture, il est possible d'ajouter ou de supprimer des fichiers. Notez qu'un utilisateur peut supprimer des fichiers d'un répertoire s'il a les droits en écriture sur celui-ci, même s'il n'a pas la permission de modifier le fichier qu'il supprime. Si un utilisateur a les droits d'exécution sur un répertoire, il a le droit d'y entrer et d'accéder aux fichiers qu'il contient. Sans droit d'exécution sur un répertoire, un utilisateur ne peut pas accéder aux fichiers qu'il contient, indépendamment des droits qu'il détient sur les fichiers eux-mêmes.

Pour conclure, observons comment le noyau décide d'autoriser ou non un processus à accéder à un fichier donné. Tout d'abord, il détermine si l'utilisateur demandant l'accès est le propriétaire du fichier, un membre du groupe propriétaire ou quelqu'un d'autre. La catégorie dans laquelle tombe l'utilisateur est utilisée pour déterminer quel ensemble de bits lecture/écriture/exécution est vérifié. Puis, le noyau contrôle l'opération effectuée par rapport aux permissions accordées à l'utilisateur(Le noyau peut également refuser l'accès à un fichier si un répertoire dans le chemin du fichier est inaccessible. Par exemple, si un processus n'a pas le droit d'accéder au répertoire /tmp/private, il ne pourra pas lire /tmp/private/data, même si les permissions de ce dernier sont définies de façon à l'y autoriser.).

Il y a une exception qu'il convient de signaler: les processus s'exécutant en tant que root (avec l'user ID~0) sont toujours autorisés à accéder à n'importe quel fichier, quelques soient les permissions qui y sont associées.

10-3-1. Faille de sécurité: les Programmes non Exécutables

Voici un premier exemple de situation ou la sécurité se complique. Vous pourriez penser que si vous interdisez l'exécution d'un programme, personne ne pourra le lancer. Après tout, c'est ce que sous-entend l'interdiction d'exécution. Mais un utilisateur ingénieux pourrait copier le programme, modifier les permissions pour rendre la copie exécutable et la lancer! Si vous interdisez l'exécution de programmes sans interdire aux utilisateurs de les copier, vous créez une faille de sécurité ? un moyen pour les utilisateurs de faire des choses que vous n'aviez pas prévu.

10-3-2. Sticky Bits

En plus des permissions en lecture, écriture et exécution, il existe un bit magique appelé sticky bit(Ce terme est anachronique; il remonte à un temps où l'activation du sticky bit (bit « collant ») permettait de conserver un programme en mémoire même lorsqu'il avait terminé de s'exécuter. Les pages allouées pour le programme étaient « collées » en mémoire. Les sticky bits sont également parfois appelés « bits de rappel ».). Ce bit ne concerne que les répertoires.

Un répertoire pour lequel le sticky bit est actif ne vous autorise à détruire un fichier que si vous en êtes le propriétaire. Comme nous l'avons dit précédemment, vous pouvez normalement supprimer un fichier si vous avez un accès en écriture sur le répertoire qui le contient, même si vous n'en êtes pas le propriétaire. Lorsque le sticky bit est activé, vous devez toujours avoir les droits en écriture sur le répertoire, mais vous devez en plus être le propriétaire du fichier que vous voulez supprimer.

Seuls certains répertoires sur un système GNU/Linux ont le sticky bit positionné. Par exemple, le répertoire /tmp, dans lequel tout utilisateur peut placer des fichiers temporaires, en fait partie. Ce répertoire est spécifiquement conçu pour pouvoir être utilisé par tous les utilisateur, tout le monde doit donc y écrire. Mais il n'est pas souhaitable qu'un utilisateur puisse supprimer les fichiers d'un autre, le sticky bit est donc activé pour ce répertoire. De cette façon, seul le propriétaire (ou root, bien sûr) peut supprimer le fichier.

Vous pouvez voir que le sticky bit est actif grâce au t à la fin de la liste des permissions si vous lancez ls sur /tmp:

 
Sélectionnez

% ls -ld /tmp
drwxrwxrwt   12 root root 2048 Jan 24 17:51 /tmp

La constante correspondante à utiliser avec stat et chmod est S_ISVTX.

Si votre programme crée des répertoires qui se comportent comme /tmp, c'est-à-dire que beaucoup de personne doivent y écrire sans pouvoir supprimer les fichier des autres, vous devez activer le sticky bit sur ce répertoire. Vous pouvez le faire en utilisant la commande chmod de cette façon:

 
Sélectionnez

% chmod o+t répertoire

Pour le faire par programmation, appelez chmod avec le drapeau de mode S_ISVTX. Par exemple, pour activer le sticky bit du répertoire spécifié par dir_path et donner un accès complet à tous les utilisateur, effectuez l'appel suivant:

 
Sélectionnez

chmod (dir_path, S_IRWXU | S_IRWXG | S_IRWXO | S_ISVTX);

10-4. ID Réels et Effectifs

Jusqu'à maintenant, nous avons parlé de l'UID et du GID associés avec un processus comme s'il n'y en avait qu'un seul de chaque. Mais, en réalité, ce n'est pas aussi simple.

Chaque processus a en réalité deux user ID: l'user ID effectif et l'user ID réel (bien sûr, il y a également un group ID effectif et un group ID réel; presque tout ce qui est vrai pour les user ID l'est également pour les group ID). La plupart du temps, le noyau ne se préoccupe que du user ID effectif. Par exemple, si un processus tente d'ouvrir un fichier, le noyau vérifie l'user ID effectif pour décider s'il laisse le processus accéder au fichier.

Les fonctions geteuid et getegid décrites précédemment renvoient l'user ID et le group ID effectifs. Les fonctions analogues getuid et getgid renvoient l'user ID et le group ID réels.

Si le noyau ne s'occupe que de l'user ID effectif, il ne semble pas très utile de faire la distinction entre user ID réel et effectif. Cependant, il y a un cas très important dans lequel le user ID réel est pris en compte. Si vous voulez changer l'user ID effectif d'un processus en cours d'exécution, le noyau vérifie l'user ID réel et l'user ID effectif.

Avant d'observer comment vous pouvez changer l'user ID effectif d'un processus, examinons pourquoi vous pourriez vouloir le faire en reprenant l'exemple de notre application de comptabilité. Supposons qu'il y ait un processus serveur qui ait besoin de consulter tout fichier présent sur le système, peu importe qui l'ait créé. Un tel processus doit s'exécuter en tant que root car lui seul est sûr de pouvoir accéder à n'importe que fichier. Mais supposons maintenant qu'une requête arrive de la part d'un utilisateur particulier (disons mitchell) pour accéder à des fichiers quelconques. Le processus serveur pourrait examiner avec attention les permissions associées avec les fichiers concernés et essayer de déterminer si mitchell devrait être autorisé à accéder à ces fichiers. Mais cela signifierait dupliquer tous les traitements que le noyau ferait de toutes façons. Réimplémenter cette logique serait complexe, sujet à des erreurs et pénible.

Une meilleure approche est simplement de modifier temporairement l'user ID effectif du processus pour ne plus qu'il soit celui de root mais celui de mitchell puis d'essayer d'effectuer les opérations demandées. Si mitchell n'a pas le droit d'accéder aux données, le noyau interdira l'accès au processus et renverra des informations adéquates sur l'erreur. Une fois que les opérations demandées par mitchell sont terminées, le processus peut récupérer son user ID effectif original qui est root.

Les programmes qui authentifient les utilisateurs lorsqu'ils essaient de se connecter tirent eux aussi avantage de cette possibilité de modifier les user ID. Ces programmes s'exécutent en tant que root ? lorsque l'utilisateur saisit un login et un mot de passe, le programme de connexion vérifie le nom d'utilisateur et le mot de passe dans la base de données du système. Puis il change à la fois ses user ID réel et effectif afin de devenir cet utilisateur. Enfin, le programme de connexion appelle exec pour lancer le shell de l'utilisateur, ce qui permet à l'utilisateur d'avoir un environnement dans lequel les user ID réel et effectif sont les siens.

La fonction utilisée pour modifier les user ID d'une processus est setreuid (il y a, bien sûr, une fonction setregid similaire). Cette fonction prend deux arguments. Le premier argument est l'user ID réel désiré; le second est l'user ID effectif demandé. Par exemple, voici comment vous échangeriez les user ID réel et effectif:

 
Sélectionnez

setreuid (geteuid(), getuid ());

Bien sûr, le noyau ne laisse pas n'importe quel processus changer son user ID. Si un processus pouvait modifier son user ID effectif à volonté, alors, toute personne pourrait facilement prendre l'identité de n'importe quel autre utilisateur simplement en changeant l'user ID effectif de l'un de ses processus. Le système laissera un processus disposant d'un user ID effectif de 0 modifier son user ID à sa guise (encore une fois, remarquez la puissance dont dispose un processus s'exécutant en tant que root! Un processus dont l'user ID effectif est 0 peut faire tout ce qu'il lui plaît). Tout autre processus, par contre, ne peut faire que l'une des actions suivantes:

  • définir son user ID effectif pour qu'il soit le même que son user ID réel ;
  • définir son user ID réel pour qu'il soit le même que son user ID effectif ;
  • échanger les deux identifiants.

La première possibilité serait utilisée par notre système de comptabilité lorsqu'il a terminé d'accéder au fichiers en tant que mitchell et veut redevenir root. La seconde par un programme de connexion une fois qu'il a défini l'user ID effectif pour qu'il soit celui de l'utilisateur qui s'est connecté. Définir l'user ID réel permet de s'assurer que l'utilisateur ne pourra jamais redevenir root. L'échange des deux identifiants est avant tout une fonctionnalité historique, les programmes modernes l'utilisent rarement.

Vous pouvez passer~-1 à la place de l'un des deux arguments de setreuid si vous ne voulez pas modifier l'user ID correspondant. Il existe également une fonction raccourci appelée seteuid. Cette fonction définit l'user ID effectif mais ne modifie pas l'user ID réel. Les deux instructions suivantes font toutes deux la même chose:

 
Sélectionnez

seteuid (id);
setreuid (-1, id);

10-4-1. Programmes Setuid

En utilisant les techniques présentées ci-dessus, vous êtes en mesure de créer des processus root qui s'exécutent sous une autre identité de façon temporaire puis redeviennent root. Vous pouvez également faire abandonner tous ses privilèges à un processus root en redéfinissant ses user ID réel et effectif.

Voici une énigme: un processus non root peut il devenir root? Cela semble impossible, en utilisant les techniques précédentes, mais voici une preuve que ça l'est:

 
Sélectionnez

% whoami
mitchell
% su
Password: ...
% whoami
root

La commande whoami est similaire à id, excepté qu'elle n'affiche que l'user ID effectif, pas les autres informations. La commande su vous permet de devenir le superutilisateur si vous connaissez le mot de passe root.

Comment fonctionne su? Comme nous savons que le shell initial s'exécute avec des user ID réel et effectif qui sont ceux de mitchell, setreuid ne nous permettra pas de les changer.

L'astuce est que le programme su est un programme setuid. Cela signifie que lorsqu'il s'exécute, son user ID effectif sera celui du propriétaire du fichier et non l'user ID du processus qui effectue l'appel exec (l'user ID réel est cependant toujours déterminé par ce dernier). Pour créer un programme setuid, utilisez la commande chmod +s ou l'option S_ISUID si vous appelez chmod par programmation(Bien sûr, il existe la notion parallèle de programme setgid. Lorsqu'un tel programme s'exécute, son group ID effectif est celui du groupe propriétaire du fichier. La plupart des programmes setuid sont également des programmes setgid.).

Étudions le programme du Listing setuidtest.

Programme de Démonstration de setuid setuid-test.c
Sélectionnez

#include <stdio.h>
#include <unistd.h>

int main ()
{
  printf ("uid=%d euid=%d\n", (int) getuid (), (int) geteuid ());
  return 0;
}

Supposons maintenant que ce programme est en mode setuid et que root en est le propriétaire. Dans ce cas, la sortie de ls -l ressemblerait à cela:

 
Sélectionnez

-rwsrws--x 1 root root 11931 Jan 24 18:25 setuid-test

Le bit s indique que le fichier n'est pas seulement exécutable (comme l'indiquerait un x) mais qu'il est également setuid et setgid. Lorsque nous utilisons ce programme, il affiche quelque chose de ce genre:

 
Sélectionnez

% whoami
mitchell
% ./setuid-test
uid=501 euid=0

Notez que l'user ID effectif est à 0 lorsque le programme s'exécute.

Vous pouvez utiliser la commande chmod avec les arguments u+s ou g+s pour activer les bits setuid et setgid sur un fichier exécutable, respectivement ? par exemple:

 
Sélectionnez

% ls -l program
-rwxr-xr-x    1 samuel csl 0 Jan 30 23:38 program
% chmod g+s program
% ls -l program
-rwxr-sr-x    1 samuel csl 0 Jan 30 23:38 program
% chmod u+s program
% ls -l program
-rwsr-sr-x    1 samuel csl 0 Jan 30 23:38 program

Vous pouvez également utiliser l'appel chmod avec les indicateurs de mode S_ISUID et S_ISGID.

su est capable de modifier l'user ID effectif par le biais de ce mécanisme. Il s'exécute initialement avec un user ID effectif à 0. Puis il vous demande un mot de passe. Si le mot de passe concorde avec celui de root, il positionne son user ID réel de sorte qu'il soit celui de root puis lance un nouveau shell. Dans le cas contraire, il se termine, vous renvoyant à votre shell d'utilisateur non privilégié.

Observons les permissions du programme su:

 
Sélectionnez

% ls -l /bin/su
-rwsr-xr-x    1 root root 14188 Mar 7 2000 /bin/su

Notez que root est le propriétaire et que le bit setuid est actif.

Remarquez que su ne change pas réellement l'user ID du shell à partir duquel il a été lancé. Au lieu de cela, il lance un nouveau shell avec le nouvel user ID. Le shell original est bloqué jusqu'à ce que le nouveau se termine.

10-5. Authentifier les utilisateurs

Souvent, lorsque vous créez un programme setuid, vous souhaitez n'en autoriser l'accès qu'à certains utilisateurs. Par exemple, le programme su ne vous laisse devenir root que si vous disposez du mot de passe correspondant. Le programme vous oblige à prouver que vous pouvez devenir root avant de vous permettre de faire quoi que ce soit. Ce mécanisme est appelé authentification ? le programme su vérifie que vous êtes celui que vous prétendez être.

Si vous administrez un système très sécurisé, l'authentification des utilisateurs par le biais d'un simple mot de passe ne vous satisfera probablement pas. Les utilisateurs ont tendence à noter leur mot de passe et les personnes mal intentionnées à les découvrir. Les utilisateurs choisissent généralement des mots de passe comme leur date de naissance, le nom de leur animal de compagnie, etc(On a découvert que les administrateurs système avaient tendance à choisir le mot dieu pour mot de passe plutôt que n'importe quel autre (pensez-en ce que vous voulez). Aussi, si vous avez besoin d'un accès root sur une machine et que l'administrateur n'est pas là, une inspiration divine pourra peut être vous aider.). Les mot de passe ne sont finalement pas une bonne garantie de sécurité.

Par exemple, beaucoup d'organisations utilisent désormais un système de mots de passe à « usage unique » générés par des cartes d'identité électronique que les utilisateurs gardent sur eux. Le même mot de passe ne peut être utilisé deux fois et vous ne pouvez obtenir de mot de passe valide à partir de la carte qu'en entrant un code d'identification. L'attaquant doit donc obtenir la carte et le code pour accéder au système. Dans des complexes extrêmement sécurisés, les scan rétiniens et d'autres types de systèmes d'identification biométriques sont utilisés.

Si vous écrivez un programme qui doit authentifier ses utilisateurs, vous devriez permettre à l'administrateur système de sélectionner le moyen d'authentification qu'il souhaite utiliser. GNU/Linux propose une bibliothèque très utile pour vous faciliter la tâche. Ce mécanisme, appelé Pluggable Authentication Modules (Modules d'Authentification Enfichable), ou PAM, facilite l'écriture d'application qui authentifient leurs utilisateurs comme le désire l'administrateur système.

Il est plus simple de comprendre le fonctionnement de PAM en étudiant une application PAM simple. Le Listing pam illustre l'utilisation de PAM.

Exemple d'Utilisation de PAM pam.c
Sélectionnez

#include <security/pam_appl.h>
#include <security/pam_misc.h>
#include <stdio.h>

int main ()
{
  pam_handle_t* pamh;
  struct pam_conv pamc;

  /* Initialise la conversation PAM. */
  pamc.conv = &amp;misc_conv;
  pamc.appdata_ptr = NULL;
  /* Démarre une nouvelle session d'authentification. */
  pam_start ("su", getenv ("USER"), &amp;pamc, &amp;pamh);
  /* Authentifie l'utilisateur. */
  if (pam_authenticate (pamh, 0) != PAM_SUCCESS)
    fprintf (stderr, "Échec de l'authentification !\n");
  else
    fprintf (stderr, "Authentification OK.\n");
  /* Fini. */
  pam_end (pamh, 0);
  return 0;
}

Pour compiler ce programme, vous devez le lier à deux bibliothèques: libpam et une bibliothèque utilitaire appelée libpam_misc:

 
Sélectionnez

% gcc -o pam pam.c -lpam -lpam_misc

Ce programme commence par construire un objet de conversation PAM. Cet objet est utilisé par la bibliothèque PAM lorsqu'elle a besoin d'obtenir des informations de la part des utilisateurs. La fonction misc_conv utilisée dans cet exemple est une fonction standard utilisant le terminal pour les entrées sorties. Vous pouvez écrire votre propre fonction qui affiche une boîte de dialogue, utilise la parole pour les entrées/sorties ou propose même des méthodes d'interactions plus exotiques. Le programme appelle ensuite pam_start. Cette fonction initialise la bibliothèque PAM. Le premier argument est un nom de service. Vous devez utiliser un nom qui identifie de façon unique votre application. Par exemple, si votre application s'appelle whizbang, vous devriez utiliser ce nom pour le service. Cependant, le programme ne fonctionnera probablement pas à moins qu'un administrateur système ne configure explicitement le système pour fonctionner avec votre service. Revenons à notre exemple, nous utilisons le service su, qui indique que notre programme authentifie les utilisateurs de la même façon que la commande su. Vous ne devriez pas utiliser cette technique dans un programme réel. Sélectionnez un nom de service qui vous est propre et concevez vos scripts d'installation pour aider l'administrateur à configurer PAM correctement pour votre application.

Le second argument est le nom de l'utilisateur que vous voulez authentifier. Dans cet exemple, nous utilisons la valeur de la variable d'environnement USER (en théorie, il s'agit du nom d'utilisateur correspondant à l'user ID effectif du processus courant, mais ce n'est pas toujours le cas). Dans la plupart des programmes réels, vous afficheriez une invite pour saisir le nom d'utilisateur. Le troisième argument est la conversation PAM, que nous avons présentée précédemment. L'appel à pam_start renseigne le handle passé en quatrième argument. Passez ce handle aux appels ultérieurs aux fonctions de la bibliothèque PAM.

Ensuite, le programme appelle pam_authenticate. Le second argument vous permet de spécifier diverses options; la valeur 0 demande l'utilisation des valeurs par défaut. La valeur de retour de cette fonction indique le résultat de l'authentification.

Finalement, le programme appelle pam_end pour libérer toutes les structures de données allouées.

Supposons que le mot de passe pour l'utilisateur courant soit « password » (un mot de passe extrêmement faible). Alors, l'exécution de ce programme avec le mot de passe correct produit le comportement attendu:

 
Sélectionnez

% ./pam
Password: password
Authentification OK.

Si vous exécutez ce programme dans un terminal, le mot de passe n'apparaîtra probablement pas lorsque vous le saisirez: il est masqué pour éviter qu'une autre personne ne puisse l'apercevoir alors que vous l'entrez.

Par contre, si un hacker tente d'utiliser un mauvais mot de passe, la bibliothèque PAM signalera l'échec correctement:

 
Sélectionnez

% ./pam
Password: raté
Échec de l'authentification !

Les bases que nous avons présenté sont suffisantes pour la plupart des programmes simples. Une documentation complète sur le fonctionnement de PAM est disponible sous /usr/doc/pam sur la plupart des systèmes GNU/Linux.

10-6. Autres failles de sécurité

Bien que ce chapitre présente quelques failles de sécurité répandues, vous ne devez en aucun cas compter sur ce livre pour couvrir toutes les failles possibles. Beaucoup on déjà été trouvés et beaucoup plus attendent de l'être. Si vous essayez d'écrire du code sécurisé, il n'y a réellement pas d'autre solution que de faire appel à un expert pour un audit de code.

10-6-1. Dépassement de tampon

Pratiquement toutes les applications Internet majeures, y compris sendmail, finger, tal et d'autres, ont a un moment donné été victimes de failles dites de dépassement de tampon.

Si vous écrivez du code destiné à être exécuté en tant que root, vous devez absolument être familier avec ce type de failles de sécurité. Cela s'applique également si vous écrivez un programme qui utilise les mécanismes de communication interprocessus. Si vous écrivez un programme qui lit des fichiers (ou pourrait lire des fichiers) vous devez là aussi connaître les concepts de cette faille. Ce dernier critère s'applique à presque tous les programmes. Fondamentalement, si vous avez l'intention d'écrire des applications GNU/Linux, vous devez connaître ce type de failles.

L'idée sous-jacente d'une attaque par dépassement de tampon est de faire exécuter à un programme du code qu'il n'était pas censé exécuter. Le mode opératoire habituel est d'écraser une partie de la pile du processus. La pile du programme contient, entre autres, l'adresse mémoire à laquelle le programme doit transférer le contrôle à la fin de la fonction en cours. Ainsi, si vous placez le code que vous voulez exécuter quelque part en mémoire et que vous modifiez l'adresse de retour pour pointer à cet emplacement, vous pouvez faire exécuter n'importe quoi au programme. Lorsque le programme terminera la fonction en cours d'exécution, il sautera vers le nouveau code et exécutera ce qu'il contient, avec les privilèges du processus en cours. Il est clair que si le programme s'exécute en tant que root, ce serait un désastre. Si le processus s'exécute avec les privilèges d'un autre utilisateur, ce n'est un désastre « que » pour cetutilisateur ? et par conséquent pour les utilisateurs dépendants de ses fichiers.

Si le programme s'exécute en tant que démon en attente de connexions réseau, la situation est encore pire. Un démon s'exécute habituellement en tant que root. S'il contient des bugs de type dépassement de tampon, n'importe quelle personne pouvant se connecter à l'ordinateur exécutant le démon peut en prendre le contrôle en envoyant une séquence de données au démon via le réseau. Un programme qui n'utilise pas les communications réseau est plus sûr car seuls les utilisateurs disposant d'un compte sur la machine peuvent l'attaquer.

Les versions de finger, talk et sendmail concernées par ce type de bug partageaient toutes la même faille. Toutes utilisaient un tampon de taille fixe pour lire une chaîne, ce qui impliquait une limite supérieure constante pour la taille de la chaîne, mais permettaient quand même aux clients d'envoyer des chaînes plus grandes que le tampon. Voici le genre de code qu'elles pouvaient contenir:

 
Sélectionnez

#include <stdio.h>

int main ()
{
  /* Personne de censé n'aurait plus de 32 caractères dans son nom
     d'utilisateur. De plus, il me semble qu'UNIX ne permet que des
     nom de 8 caractères. Il y a donc suffisamment de place. */
  char username[32];
  /* Demande le nom de l'utilisateur. */
  printf ("Saisisez votre identifiant de connexion : ");
  /* Lit une ligne saisie par l'utilisateur. */
  gets (username);
  /* Traitements divers... */

  return 0;
}

L'utilisation conjointe d'un tampon de 32 caractères et de la fonction gets ouvre la porte à un dépassement de tampon. La fonction gets lit la saisie de l'utilisateur jusqu'à ce qu'un caractère de nouvelle ligne apparaîsse et stocke le résultat dans le tampon username. Les commentaires dans le code sont corrects en ce sens que les utilisateurs ont généralement des identifiants courts, aucun utilisateur bien intentionné ne saisirait plus de 32 caractères. Mais vous écrivez un logiciel sécurisé, vous devez adopter le point de vue d'un attaquant. Dans ce cas, l'attaquant pourrait délibérément saisir un nom d'utilisateur très long. Les variables locales comme username sont stockées dans la pile, aussi, en dépassant les limites du tableau, il est possible de placer des octets arbitraires sur la pile au delà de la zone réservée à la variable username. Le nom d'utilisateur dépasse alors le tampon et écrase une partie de la pile, permettant une attaque telle que celle décrite précédemment.

Heureusement, il est facile d'éviter les dépassements de tampon. Lorsque vous lisez des chaînes, vous devriez toujours utiliser soit une fonction, comme getline, qui alloue dynamiquement suffisamment d'espace soit une fonction qui interrompt la lecture lorsque le tampon est plein. Voici un exemple d'utilisation de getline:

 
Sélectionnez

Cet appel utilise automatiquement malloc pour allouer un tampon suffisamment grand pour contenir la ligne et vous le renvoie. Vous ne devez pas oublier d'appeler free pour libérer le tampon afin d'éviter les fuites mémoire.

Vous vous faciliterez la vie si vous utiliser le C++ ou un autre langage proposant des primitives simples pour lire les saisies utilisateur. En C++, par exemple, vous pouvre utiliser cette simple instruction:

 
Sélectionnez

string username;
getline (cin, username);

La chaîne username sera également désallouée automatiquement, vous n'avez pas besoin d'appeler free(Certains programmeurs pensent que le C++ est un langage horrible et compliqué.Leurs arguments sur l'héritage multiple et d'autres complication ont un certain mérite, mais il est plus simple d'écrire du code évitant les dépassements de tampon et autres problèmes similaires en C++ qu'en C.).

Bien sûr, les dépassements de tampon peuvent survenir avec n'importe quel tableau dimensionné de façon statique, pas seulement avec les chaînes de caractères. Si vous voulez produire un code sécurisé, vous ne devriez jamais écrire dans une structure de données, sur la pile ou ailleurs, sans vérifier que vous n'allez pas dépasser ses limites.

10-6-2. Conditions de concurrence critique dans /tmp

Une autre problème très répandu converne la création de fichiers avec des noms prédictibles, typiquement dans le répertoire /tmp. Supposons que votre programme prog, qui s'exécute avec les droits root, crée toujours un fichier temporaire appelé /tmp/prog et y écrive des informations vitales. Un utilisateur mal intentionné pourrait créer un lien symbolique sous /tmp/prog vers n'importe quel fichier du système. Lorsque votre programme tente de créer le fichier, l'appel système open n'échouera pas. Cependant, les données que vous écrirez n'iront pas vers /tmp/prog mais seront écrites dans le fichier choisi par l'attaquant.

On dit de ce genre d'attaque qu'elle exploite un condition de concurrence critique. Il y a une concurrence implicite entre vous et l'attaquant. Celui qui arrive à créer le fichier en premier gagne.

Cette attaque est généralement utilisée pour détruire des éléments importants du système de fichiers. En créant les liens appropriés, l'attaquant peut utiliser un programme s'exécutant en tant que root croyant écrire dans un fichier temporaire pour écraser un fichier système important. Par exemple, en créant un lien symbolique vers /etc/passwd, l'attaquant peut effacer la base de données des mots de passe du système. Il existe également des moyens pour l'attaquant d'obtenir un accès root en utilisant cette technique.

Une piste pour éviter ce genre d'attaque serait d'utiliser un nom aléatoire pour le fichier. Par exemple, vous pourriez utiliser /dev/random pour injecter une partie aléatoire dans le nom du fichier. Cela complique bien sûr la tâche de l'attaquant pour deviner le nom du fichier, mais cela ne l'en empêche pas. Il pourrait créer un nombre conséquent de liens symboliques en utilisant beaucoup de nom potentiels. Même s'il doit essayer 10 000 fois avant d'obtenir des conditions de concurrence critique, cette seule fois peut être désastreuse.

Une autre approche est d'utiliser l'option O_EXCL lors de l'appel à open. Cette option provoque l'échec de l'ouverture si le fichier existe déjà. Malheureusement, si vous utilisez le Network File System (NFS), ou si un utilisateur de votre programme est susceptible d'utiliser NFS, cette approche n'est pas assez robuste car O_EXCL n'est pas fiable sur un système de fichier NFS. Vous ne pouvez pas savoir avec certitude si votre code sera utilisé sur un système disposant de NFS, aussi, si vous êtes paranoïaque, ne vous reposez pas sur O_EXCL.

Dans le Chapitre logicielsQualite, Chapitre logicielsQualite, Section fichierstemporaires, « Utilisation de Fichiers temporaires », nous avons présenté mkstemp. Malheureusement, sous Linux, mkstemp ouvre le fichier avec l'option O_EXCL après avoir déterminé un nom suffisamment dur à deviner. En d'autres termes, l'utilisation de mkstemp n'est pas sûre si /tmp est monté via NFS(Bien sûr, si vous êtes administrateur système, vous ne devriez pas monter un système NFS sur /tmp.). L'utilisation de mkstemp est donc mieux que rien mais n'est pas totalement sûre.

Une approche qui fonctionne est d'utiliser lstat sur le nouveau fichier (lstat est présenté dans la Section B.2, « stat »). La fonction lstat est similaire à stat, excepté que si le fichier est lien symbolique, lstat vous donne des informations sur ce lien et non sur le fichier vers lequel il pointe. Si lstat vous indique que votre nouveau fichier est un fichier ordinaire, pas un lien symbolique, et que vous en êtes le propriétaire, alors tout devrait bien se passer.

Le Listing tempfile2 présente une fonction tentant d'ouvrir un fichier dans /tmp de façon sécurisée. Les auteurs de ce livre ne l'ont pas fait audité de façon professionnelle et ne sont pas non plus des experts en sécurité, il y a donc de grandes chances qu'elle ait une faiblesse. Nous ne vous recommandons pas son utilisation sans l'avoir faite auditer, mais elle devrait vous convaincre que l'écriture de code sécurisé est complexe. Pour vous dissuader encore plus, nous avons délibérément défini l'interface de façon à ce qu'elle soit complexe à utiliser dans un programme réel. La vérification d'erreurs tient une place importante dans l'écriture de logiciels sécurisés, nous avons donc inclus la logique de contrôle d'erreurs dans cet exemple.

Créer un Fichier Temporaire temp-file.c
Sélectionnez

#include <fcntl.h>
#include <stdlib.h>
#include <sys/stat.h>
#include <unistd.h>

/* Renvoie le descripteur de fichier d'un nouveau fichier temporaire.
   Le fichier pourra être lu ou écrit par l'user ID effectif du processus
   courant et par personne d'autre.

   Renvoie -1 si le fichier temporaire ne peut pas être créé.  */

int secure_temp_file ()
{
  /* Ce descripteur de fichier pointe vers /dev/random et nous permet
     de disposer d'une bonne source de nombres aléatoires. */
  static int random_fd = -1;
  /* Entier aléatoire. */
  unsigned int random;
  /* Tampon utilisé pour convertir random en chaîne de caractères.
     Le tampon à une taille fixe, ce qui signifie que nous sommes
     potentiellement vulnérables à un bug de dépassement de tampon si
     les entiers de la machine d'exécution tiennent sur un nombre
     *conséquent* de bits. */
  char filename[128];
  /* Descripteur de fichier du nouveau fichier temporaire. */
  int fd;
  /* Informations sur le nouveau fichier. */
  struct stat stat_buf;

  /* Si nous n'avons pas encore ouvert /dev/random nous le faisons
     maintenant. Cette façon de faire n'est pas threadsafe. */
  if (random_fd == -1) {
  /* Ouvre /dev/random. Notez que nous supposons ici que /dev/random est
     effectivement une source de bits aléatoire et non pas un fichier
     rempli de zéros placé ici par l'attaquant. */
  random_fd = open ("/dev/random", O_RDONLY);
  /* Abandonne si l'on ne peut pas ouvrir /dev/random. */
  if (random_fd == -1)
    return -1;
  }

  /* Lit un entier à partir de /dev/random. */
  if (read (random_fd, &amp;random, sizeof (random)) !=
      sizeof (random))
    return -1;
  /* Crée un fichier à partir du nombre aléatoire. */
  sprintf (filename, "/tmp/%u", random);

  /* Tente d'ouvrir le fichier. */
  fd = open (filename,
             /* Nous utilisons O_EXECL, 
                même si cela ne fonctionne pas avec NFS. */
             O_RDWR | O_CREAT | O_EXCL,
             /* Personne ne doit pouvoir lire ou écrire dans le fichier. */
             S_IRUSR | S_IWUSR);
  if (fd == -1)
    return -1;
  /* Appelle lstat sur le fichier afin de s'assurer qu'il
     ne s'agit pas d'un lien symbolique. */
  if (lstat (filename, &amp;stat_buf) == -1)
    return -1;
  /* Si le fichier n'est pas un fichier traditionnel, quelqu'un
     a tenté de nous piéger. */
  if (!S_ISREG (stat_buf.st_mode))
    return -1;
  /* Si le fichier ne nous appartient pas, quelqu'un d'autre pourrait
     le supprimer, le lire ou le modifier alors que nous nous en
     servons. */
  if (stat_buf.st_uid != geteuid () || stat_buf.st_gid != getegid ())
    return -1;
  /* Si il y a d'autres bits de permissions actifs,
     quelque chose cloche. */
  if ((stat_buf.st_mode &amp; ~(S_IRUSR | S_IWUSR)) != 0)
    return -1;

  return fd;
}

Cette fonction appelle open pour créer le fichier puis appelle lstat quelques lignes plus loin pour s'assurer que le fichier n'est pas un lien symbolique. Si vous réfléchissez attentivement, vous réaliserez qu'il semble y avoir une condition de concurrence critique dans ce cas. En effet, un attaquant pourrait supprimer le fichier et le remplacer par un lien symbolique entre le moment où nous appelons open et celui où nous appelons lstat. Cela n'aurait pas d'impact direct sur cette fonction car nous avons déjà un descripteur de fichier ouvert pointant sur le nouveau fichier, mais nous indiquerions une erreur à l'appelant. Cette attaque ne causerait pas de dommages directs mais rendrait impossible le fonctionnement de l'appelant. Une telle attaque est dite déni de service (DoS, Denial of Service).

Heureusement, le sticky bit vient à notre aide. Comme le sticky bit est actif sur /tmp, personne d'autre que nous ne peut supprimer les fichiers de ce répertoire. Bien sûr, root peut toujours supprimer des fichiers, mais si l'attaquant dispose déjà des privilèges root, il n'y a rien qui puisse protéger votre programme.

Si vous choisissez de supposer que l'administrateur système est compétent, alors /tmp ne sera pas monté via NFS. Et si l'administrateur système est suffisamment stupide pour monter /tmp en NFS, il y a de bonnes chances que le sticky bit ne soit pas actif non plus. Aussi, pour la plupart des utilisations, nous pensons qu'il est sûr d'utiliser mkstemp. Mais vous devez être conscient de ces problèmes et ne devez pas vous reposer sur le fonctionne de O_EXCL pour un fonctionnement correct si le répertoire utilisé n'est pas /tmp ? pas plus que vous ne devez supposer que le sticky bit est actif ailleurs.

10-6-3. Utilisation de system ou popen

La troisième faille de sécurité que tout programme devrait avoir en tête est l'utilisation du shell pour exécuter d'autres programme. Prenons l'exemple fictif d'un serveur dictionnaire. Ce programme est conçu pour accepter les connexions venant d'Internet. Chaque client envoie un mot et le serveur indique s'il s'agit d'un mot anglais valide. Comme tout système GNU/Linux dispose d'une liste d'environ 45000 mots anglais dans /usr/share/dict/word, une façon simple de créer ce serveur est d'invoquer le programme grep, comme ceci:

 
Sélectionnez

% grep -x word /usr/dict/words

Ici, word est le mot que souhaite valider l'utilisateur. Le code de sortie de grep nous indiquera si le mot figure dans /usr/share/dict/words(Si vous ne connaissez pas grep, vous devriez consulter les pages de manuel. C'est un programme incoyablement utile.).

Le Listing grepdictionary vous montre comment vous pourriez coder la partie du serveur invoquant grep:

Cherche un Mot dans le Dictionnaire grep-dictionary.c
Sélectionnez

#include <stdio.h>
#include <stdlib.h>

/* Renvoie une valeur différente de 0 si et seulement si WORD
   figure dans /usr/dict/words. */

int grep_for_word (const char* word)
{
  size_t length;
  char* buffer;
  int exit_code;

/* Construit la chaîne "grep -x WORD /usr/dict/words". Alloue la chaîne 
   dynamiquement pour éviter les dépassements de tampon. */
  length =
    strlen ("grep -x ") + strlen (word) + strlen (" /usr/dict/words") + 1;
  buffer = (char*) malloc (length);
  sprintf (buffer, "grep -x %s /usr/dict/words", word);

  /* Exécute la commande. */
  exit_code = system (buffer);
  /* Libère le tampon. */
  free (buffer);
  /* Si grep a renvoyé 0, le mot était présent dans le dictionnaire. */
  return exit_code == 0;
}

Remarquez qu'en calculant le nombre de caractères dont nous avons besoin et en allouant le tampon dynamiquement, nous sommes sûrs d'éviter les dépassements de tampon.

Malheureusement, l'utilisation de la fonction system (décrite dans le Chapitre processus, Chapitre processus, Section utilisersystem, Section utilisersystem) n'est pas sûr. Cette fonction invoque le shell système standard pour lancer la commande puis renvoyer la valeur de sortie. Mais que se passe-t-il si un attaquant envoie un « mot » qui est fait la ligne suivante ou quelque chose du même type?

 
Sélectionnez

foo /dev/null; rm -rf /

Dans ce cas, le serveur exécutera cette commande :

 
Sélectionnez

grep -x foo /dev/null; rm -rf / /usr/dict/words

Le problème est maintenant évident. L'utilisateur a transformé une commande, l'invocation de grep, en deux commandes car le shell traite le point virgule comme un séparateur de commandes. La première commande est toujours l'invocation inoffensive de grep, mais la seconde supprime tous les fichiers du système! Même si le serveur ne s'exécute pas en tant que root, tous les fichiers qui peuvent être supprimés par l'utilisateur sous lequel s'exécute le serveur seront supprimés. Le même problème peut survenir avec popen (décrit dans la Section popenetpclose, Section popenetpclose), qui crée un pipe entre le processus père et le fils mais utilise quand même le shell pour lancer la commande.

Il y a deux façons d'éviter ces problèmes. La première est d'utiliser les fonctions de la famille exec au lieu de system et popen. Cette solution contourne le problème car les caractères considérés comme spéciaux par le shell (comme le point-virgule dans la commande précédente) ne sont pas traités lorsqu'ils apparaissent dans les arguments d'un appel à exec. Bien sûr, vous abandonnez le côté pratique de system et popen.

L'autre alternative est de valider la chaîne pour s'assurer qu'elle n'est pas dangereuse. Dans l'exemple du serveur dictionnaire, vous devriez vous assurer que le mot ne contient que des caractères alphabétiques, en utilisant la fonction isalpha. Si elle ne contient pas d'autres caractères, il n'y a aucun moyen de piéger le shell en lui faisant exécuter une autre commande. N'implémentez pas la vérification en recherchant des caractères dangereux ou inattendus; il est toujours plus sûr de rechercher explicitement les caractères dont vous savez qu'ils sont sûrs que d'essayer d'anticiper sur tous les caractères pouvant être problématiques.


précédentsommairesuivant

Vous avez aimé ce tutoriel ? Alors partagez-le en cliquant sur les boutons suivants : Viadeo Twitter Facebook Share on Google+   

Copyright © 2007 Mark Mitchell, Jeffrey Oldham et Alex Samuel. Ce document ne peut être distribué que dans le respect des termes et conditions définies par l?Open Poublication License, v1.0 ou ultérieure (la dernière version est disponible sur http://www.opencontent.org/openpub/).