TD 5 INF422 — Shell et système de fichiers

Introduction      FAQ   Glossary   Project   TD 1   TD 2   TD 3   TD 4   TD 5   TD 6   TD 7   TD 8

Retour au cours.

Objectifs :

Pré-requis :

Le but de ce TD est de créer un serveur de commandes (shell distant) permettant à des utilisateurs de se connecter et d'exécuter des commandes. Afin de protéger notre ordinateur (ou plutôt notre téléphone Android) de commandes malencontreuses (ou malveillantes!), nous veillerons à ce que ces dernières soient exécutées dans un environnement limité (commandes et fichiers accessibles contrôlés).

Auparavant, commençons par une étude élémentaire du système de fichiers.

1. Introduction au système de fichiers

Pour ce TD, vous allez utiliser quelques commandes usuelles des systèmes UNIX. Une liste de commandes a déjà été présentée au TD précédent. Vous pouvez vous référer à la page de manuel de Busybox pour une liste quasi-exhaustive qui détaille les options possibles. Des explications complètes du fonctionnement de chaque commande sont disponibles dans les pages de manuel de FreeBSD, mais, attention, beaucoup des options de ces pages ne sont pas disponibles sur les commandes plus simples fournies par BusyBox. Voici en particulier la page de manuel de sh, le shell.

Connectez-vous par telnet à Android en tant que root. Listez les fichiers contenus à la racine avec leurs permissions. Faites la même chose successivement dans /bin et /dev. Quels sont les fichiers qui ont des permissions d'exécution? Quels sont les types de fichiers trouvés dans /dev?

Donnez 2 lignes, chacune utilisant une commande différente, affichant la hiérarchie complète des répertoires et fichiers depuis /.

Si vous ne vous rappelez plus comment lister des fichiers, relisez le paragraphe "Commandes de base" du TD précédent.

Pour la dernière question, utilisez la commande de listage avec une option spéciale. L'autre commande à utiliser est une commande de recherche de fichiers (que nous vous laissons le soin de trouver dans la documentation de Busybox).

Sur tout système de type UNIX, la structure de base du système de fichiers obéit à des conventions, décrites en détail dans un standard appelé FHS (pour référence, la page wikipédia ou la page de manuel de FreeBSD ou encore le texte complet du standard).

Par exemple, /bin contient les exécutables essentiels à la maintenance du système, les autres exécutables se trouvant habituellement dans /usr/bin et /usr/local/bin. /dev contient des fichiers spéciaux représentants des périphériques du système.

Les commandes de Busybox

La plupart des commandes du répertoire /bin d'un système utilisant Busybox, comme Android, sont en fait des liens symboliques vers la commande /bin/busybox. Pour utiliser les commandes fournies par Busybox, on peut donc:

Créer un répertoire my_bin à la racine. Déplacez-y l'exécutable de la commande nice : /bin/nice. Tapez maintenant nice ls. Pourquoi la commande ne fonctionne-t-elle pas? Modifiez la variable d'environnement PATH pour que le shell cherche les commandes également dans /my_bin. La commande fonctionne-t-elle cette fois? De quel type est le fichier /my_bin/nice? Comment remédier au problème avec cp? Remettez finalement nice dans /bin et supprimez my_bin.

Lors du 1er TD, vous avez déjà rencontré /proc. Le but principal de ce répertoire est de contenir des fichiers non persistants donnant des informations sur les processus du système (d'où son nom). Sur Linux, il contient (malheureusement) aussi des fichiers qui servent à obtenir (et parfois, modifier) des informations sur le système et certains de ses paramètres. On y trouve par exemple :

Affichez le contenu du répertoire /proc/1, qui donne des informations sur le processus numéro 1. Quel est le nom de ce processus?

Affichez le contenu du fichier /proc/uptime. Attendez quelques instants et affichez-le de nouveau. Que constatez-vous? Quel est le type de ce fichier? A quoi sert la commande uptime? Lancez-là. Devinez-vous comment celle-ci est implémentée?

Dans le système de fichiers global, au niveau d'un répertoire, il est possible de monter le contenu d'un périphérique contenant un autre système de fichiers, c'est-à-dire de rendre accessible à partir de ce répertoire l'arborescence de fichiers contenus dans ce périphérique:

En réalité, cette possibilité est même une obligation: un système UNIX ne démarrera pas sans la fourniture d'un système de fichiers racine qui sera accessible depuis ("monté sur") /.

En utilisant la commande mount, affichez la liste des systèmes de fichiers actuellement montés. Expliquez pourquoi les fichiers contenus dans /proc sont spéciaux.

  1. Créez un répertoire /mnt, et un fichier texte my_file dans /mnt.
  2. Montez la partition /dev/block/mtdblock1 sur /mnt. Visualisez le contenu de /mnt, qu'est devenu le fichier my_file ?
  3. Créez un fichier my_file2 dans /mnt. Visualisez le contenu de /data, qu'observez-vous ?
  4. Au moyen de la commande umount, démontez le répertoire /mnt. Visualisez les répertoires /mnt et /data et vérifiez que tout se passe comme attendu.

  1. Montez un système de fichiers de type proc sur le répertoire /mnt. On fournira comme nom de périphérique à la commande mount un nom de fichier imaginaire. Que se passe-t-il ? Avez-vous une explication ?
  2. Essayez de créer un nouveau fichier dans /mnt. Cela fonctionne-t-il? Pourquoi ?
  3. Démontez finalement le système de fichier de type proc en utilisant umount.

2. Un serveur de commandes simple et (relativement) sécurisé

Nous allons maintenant mettre en place un serveur de commandes simple sur notre émulateur Android.

Récupérez le script rshd.sh et copiez-le dans /home sur Android. Il lit les commandes tapées par l'utilisateur et les exécute jusqu'à ce que l'utilisateur se déconnecte, grâce à une boucle while.

Lancez un serveur sur le port 10000 qui exécute ce script quand on s'y connecte. Vérifiez qu'il fonctionne correctement. Tapez finalement exit pour vous déconnecter.

Pour copier un fichier de votre ordinateur dans Android, vous pouvez utiliser la command adb push (depuis un shell de votre machine). Attention : le fichier rshd.sh qui vous est fourni contient un caractère spécial (^M ou \r). Vous ne pourrez donc pas le copier tel quel en collant son contenu dans une fenêtre Android après y avoir ouvert un éditeur ou bien lancé cat avec une redirection de la sortie standard vers un fichier, comme vous aviez pu le faire lors de précédents TDs.

Pour transformer un programme en un serveur, vous pouvez utiliser nc ou bien inetd. Consultez le TD précédent si nécessaire.

Note : les boucles en shell

Il y a 2 façons de faire une boucle en shell (sh et dérivés) :

Sécurisons maintenant ce serveur de commandes afin d'éviter que l'utilisateur ne modifie par inadvertance des fichiers importants. Pour cela, créons un répertoire qui servira de répertoire de travail à l'utilisateur connecté.

Créez un répertoire /home/wd. Modifiez le script rshd.sh pour qu'il utilise ce dernier comme répertoire courant. Vérifiez que le script fonctionne.

Pour la vérification, vous pouvez par exemple utiliser pwd ou bien créer un fichier dans /home/wd par telnet puis taper ls dans le serveur de commandes et vérifier que le fichier est bien listé.

Malgré la création d'un répertoire de travail utilisé comme répertoire courant, les utilisateurs du serveur de commandes ont toujours accès, non seulement à toutes les commandes, mais également à tous les fichiers d'Android.

Créez un fichier dans la racine par telnet puis vérifiez que vous pouvez l'atteindre par le serveur de shell en affichant son contenu puis en l'effaçant.

Pour éviter ce problème, nous allons utiliser la commande chroot DIRECTORY COMMAND, qui exécute la commande COMMAND avec le répertoire DIRECTORY comme racine. Ainsi, l'utilisateur ne pourra pas "s'échapper" de ce répertoire, en utilisant par exemple cd. On donne parfois le nom de cage ou prison (jail) à ce nouveau répertoire racine.

Testez les commandes suivantes pour comprendre comment la commande chroot fonctionne:

Parmi les commandes précédentes, quelles sont celles qui renvoient une erreur? Pouvez-vous expliquer pourquoi?

Pour pouvoir utiliser les commandes habituelles après un chroot, il va falloir créer une hiérarchie minimale contenant les commandes autorisées à l'utilisateur connecté.

Dans /home/wd, créez un répertoire /bin et copiez-y les commandes busybox, sh, ls, ps et df. A l'aide de chroot, exécutez les commandes suivantes avec /home/wd comme cage:

Testez les commandes ps et df. Pourquoi ne fonctionnent-elles pas? Comment y remédier?

Indication : si vous ne voyez pas comment répondre à la dernière question, reprenez la question sur uptime de l'exercice 1.

Pour éviter que plusieurs utilisateurs connectés en même temps n'empiètent les uns sur les autres, nous allons maintenant créer, pour chaque connexion, un nouveau répertoire de travail dédié.

Créer un fichier temporaire

La commande mktemp permet de créer un fichier ou un répertoire temporaire : le nom de ce fichier est construit de telle sorte qu'il soit différent de tous les fichiers déjà existants dans le même répertoire. La partie du nom que mktemp peut modifier pour éviter les conflits s'indique dans l'argument par des X. Le fichier ou répertoire (option -d) est ensuite créé, et la commande affiche son nom dans sa sortie standard :

$ mktemp -d /tmp/tmp.XXXXXX
/tmp/tmp.cNR0tA
$ ls /tmp
tmp.cNR0tA

Renommez le fichier /home/rshd.sh en /home/exec.sh. Écrivez un nouveau script /home/rshd.sh qui :

Tester quelles commandes il est possible d'exécuter à présent.

Il serait possible de recopier, pour chaque utilisateur se connectant à notre service, tout le répertoire /bin, qui contient les commandes du système, dans le répertoire temporaire de cet utilisateur afin que le script exec.sh donne accès à toutes les commandes. Néanmoins, cela serait inutilement coûteux en espace disque.

Utiliser la commande du pour évaluer le coût d'une telle opération. Utiliser la commande df pour évaluer le nombre maximum d'utilisateurs que le serveur pourrait donc accepter simultanément.

Au lieu de gaspiller l'espace disque avec des copies, il est possible de créer des liens vers les fichiers originaux.

Modifiez votre script rshd.sh pour qu'il crée des liens symboliques vers les commandes situées dans /bin, au lieu de les copier comme précédemment. Vous utiliserez pour cela la commande ln avec une option particulière. Votre script fonctionne-t-il? Expliquez ce qu'il se passe. Reprenez cette question en utilisant cette fois des liens durs et expliquez la différence de comportement.

Liens symboliques et liens durs

La situation ci-dessus peut être obtenue avec la suite de commandes suivante:

# mkdir /home/alice
# dd if=/dev/urandom of=/home/alice/report count=5
# mkdir /home/bob  
# ln /home/alice/report /home/bob/my_report
# ln -s /home/alice/report /home/bob/alice_report
# ln -s ../bob/report /home/alice/bob_report
# ln -s ../bob/my_report /home/alice/bob_report

Testez les commandes suivantes, et interprétez leurs résultats:

  1. cp /home/alice/report /home/alice/report2
    ls -li /home/alice/report* /home/bob/my_report
  2. echo Bonjour >> /home/alice/report
    ls -li /home/alice/report* /home/bob/my_report
  3. rm /home/alice/report
    cp /home/bob/alice_report /home/bob/report2
  4. cp /home/bob/my_report /home/bob/report2
  5. ln -s /home/bob/alice_report /home/alice/report
    cp /home/alice/report /home/alice/report.backup

3. Création d'une partition virtuelle

Afin d'éviter de créer des liens dans le répertoire de travail à chaque fois qu'un utilisateur se connecte, nous allons créer à l'avance un nouveau système de fichiers contenant les commandes disponibles qui sera stocké dans un fichier normal et que nous monterons ensuite dans le répertoire temporaire de chaque utilisateur, comme s'il s'agissait d'un disque externe ou d'une partition d'un disque dur. Le répertoire sera monté en mode lecture-seulement pour éviter qu'un utilisateur ne puisse modifier son contenu.

La première étape consiste à créer un fichier pour contenir la partition. Nous proposons d'utiliser une taille de 223 octects pour ce fichier (soit 8 Mo).

Créez un fichier /home/bin.img de taille 223 octets, en utilisant la commande dd et en utilisant comme fichier d'entrée le périphérique spécial /dev/zero.

Il nous faut maintenant construire un système de fichiers vierge dans ce fichier, comme Windows ou une distribution Linux le font automatiquement pour le disque dur lors de leur première installation.

Traditionnellement, sur un système UNIX, on crée un nouveau système de fichier en utilisant newfs. Sous Linux, la commande est mkfs et il faut préciser le type de système de fichiers à créer (ce qui spécifie la façon dont les fichiers sont stockés sur le disque). On peut aussi appeler directement le programme de création pour un type donné, comme par exemple mke2fs pour le système de fichier Ext2/Ext3 ou bien mkdosfs qui crée un système de fichier de type FAT (ancien format Windows et MS-DOS).

Téléchargez le fichier mkdosfs et installez le dans /bin avec les droits autorisant l'exécution pour tous les utilisateurs (indication : utiliser adb push sur votre ordinateur et la commande chmod dans le shell sous Android).

Maintenant que nous avons créé un système de fichiers, nous allons l'attacher à l'arborescence globale afin de pouvoir les modifier.

Le périphérique loopback

De même qu'il existe une interface réseau spéciale, appelée loopback, permettant à un programme de se connecter à un autre sur le même ordinateur en utilisant des sockets (son adresse IP est par convention 127.0.0.1), il existe sous Linux un périphérique spécial loopback permettant à l'ordinateur de considérer un de ses fichiers comme un disque externe ("block devices" car accessibles par blocs entiers). Les fichiers choisis seront présentés comme des disques au travers des fichiers spéciaux loopX, où X est un chiffre entre 0 et 9, situés dans /dev. Pour associer un fichier normal, contenant un système de fichiers, à un fichier spécial loop, il faut utiliser la commande losetup. Après cela, il est finalement possible d'attacher le système de fichiers (stocké dans le fichier normal) dans l'arborescence en utilisant mount avec comme périphérique le fichiers loop associé avec losetup.

Sur Android, il n'existe pas de fichiers /dev/loop0, /dev/loop1, /dev/loop2, etc... Cela signifie-t-il que le périphérique loopback n'est pas reconnu par le système? Comment le savoir facilement ?

Utilisez la commande mknod dans une boucle pour créer des fichiers spéciaux loop numérotés de 0 à 4. Pour cela, utilisez comme numéro mineur de périphérique le chiffre placé à la fin du nom des fichiers spéciaux loop et comme numéro majeur 7, utilisé sous Linux par loopback. Ces derniers sont des conventions servant à un périphérique (ici, loopback) pour reconnaître les fichiers spéciaux avec lesquels il interagit.

Utilisez ensuite losetup et mount pour attacher tour-à-tour vos systèmes de fichiers sur le répertoire /home/bin_img que vous aurez créé pour l'occasion. Quels sont les systèmes que vous pouvez utiliser ? Pouviez-vous le prévoir? Montez finalement un des systèmes utilisables sur /home/bin_img.

Vérifiez le type et la capacité de notre partition avec la commande df.

Indications : Pour trouver une façon de savoir si loopback est reconnu et une façon de prévoir quels systèmes de fichiers sont supportés, relisez la description du système de fichiers de type proc dans l'exercice 1.

Note : Les numéros majeurs ou mineurs n'ont plus leur signification traditionnelle sur les systèmes qui utilisent devfs ou udev (par exemple, FreeBSD ou Linux, en option).

Il ne nous reste plus qu'à remplir notre nouveau système de fichiers avec les commandes que nous autorisons l'utilisateur à appeler.

Tapez la commande suivante:

cp -P /bin/* /home/bin_img/

Quel est le sens de l'argument -P de cp ? Pourquoi l'utilise t'on ?

Pourquoi cette commande produit-elle des erreurs ? Vérifier la liste des fichiers copiés dans notre partition ainsi que l'espace restant.

Une fois cette commande exécutée, on démontera le système de fichiers.

Nous pouvons maintenant modifier notre serveur de commandes :

Modifiez votre script rshd.sh pour que, après avoir créé un répertoire bin dans le répertoire temporaire de chaque utilisateur qui se connecte, il attache votre système de fichiers en mode lecture-seulement sur ce répertoire, au lieu de copier ou lier les commandes, avant d'exécuter la commande chroot.

N'oubliez pas de lui faire démonter le système de fichiers après l'exécution de la commande et avant de détruire le répertoire temporaire. Testez quelques commandes pour vérifier le fonctionnement.

4. Exercice bonus: Login et Répertoires utilisateurs

Dans son état actuel, notre serveur de commandes crée un nouveau répertoire de travail à chaque connexion, y construisant la hiérarchie nécessaire à une cage. Un utilisateur perd donc systématiquement ses données lors de la déconnexion. Nous souhaitons maintenant améliorer le serveur pour qu'il demande à un utilisateur un login qui lui permettra de garder et réutiliser les données associées.

Modifiez rshd.sh afin qu'à chaque connexion, le script :

Ainsi, à chaque connexion, l'utilisateur retrouvera les données précédemment stockées dans son /home. Afin d'économiser de l'espace disque, nous allons compresser les fichiers contenant le système de fichiers qui stocke les données utilisateurs.

Modifiez rshd.sh pour qu'il compresse l'image correspondant au répertoire home d'un utilisateur (/home/${LOGIN}.img) une fois qu'il s'est déconnecté et décompresse l'image quand il se reconnecte. Vous utiliserez les commandes gzip et gunzip.

Les 2 commandes indiquées ajoute ou enlève automatiquement l'extension .gz au nom du fichier. Veillez bien à changer le test d'existence du fichier image, qui conditionne la création d'un nouveau fichier ou la réutilisation des données précédentes.

Suite à cette dernière modification, notre serveur ne peut gérer plusieurs connexions simultanées d'un même utilisateur correctement, puisque son fichier image est décompressé lors de la première connexion et n'existe pas sous forme compressée le temps de la connexion, ce qui entraînera la création d'une nouvelle image écrasant l'ancienne.

Modifiez rshd.sh afin qu'il n'autorise pas une nouvelle connexion pour un utilisateur déjà connecté.

Vous pourriez par exemple tirer profit de l'information donnée à la question précédente sur le changement de nom d'un fichier compressé/décompressé, utilisant ainsi la technique classique d'un fichier marqueur d'un état particulier.

Question subsidiaire (plus difficile): expliquez pourquoi cette technique, qui semble fonctionner dans notre cas, est théoriquement boguée. Imaginez en particulier un ordonnancement des évènements qui induise un comportement non désiré.

Pour finir, nous vous proposons d'afficher quelques statistiques sur les données personnelles lorsque l'utilisateur se déconnecte.

Modifiez exec.sh pour que, lorsque l'utilisateur se déconnecte en tapant exit, lui soit affiché l'espace disque occupé par ses fichiers personnels.

Faites ensuite en sorte que, à la déconnexion, s'affiche la liste de tous les fichiers qui ont été créés ou modifiés depuis le début de la connexion.

Ajoutez-y enfin l'affichage des 3 fichiers les plus volumineux.

Voici une liste (non-exclusive) de commandes qui vous serons utiles, par ordre alphabétique: df, du, find, sort, tail et touch.

Pour savoir quels ont été les fichiers modifiés depuis le début de la connexion, vous pouvez créer un fichier "témoin" dont la date de création sera utilisée comme référence grâce à une option particulière de find.

Voilà, si vous êtes arrivé jusqu'ici, vous en savez assez pour vous débrouiller sur bien des tâches en ligne de commande sous Android et plus généralement sur un système UNIX. Vous êtes donc mûr pour en installer et en utiliser un (mais c'est probablement ce que vous faites déjà, n'est-ce pas?). Et n'oubliez pas que, sur ce type de système, en cas de trou de mémoire, vous pouvez toujours utiliser la commande man.

Bon courage!