TD 6 INF422 — Appels système et noyau

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

Retour au cours.

Objectifs :

1. Introduction

Comme il a été expliqué en cours, le processeur d'un ordinateur peut exécuter du code dans 2 modes différents. Le premier est le mode superviseur, dit aussi mode noyau, qui permet un accès à toutes les fonctionnalités de la machine sans restriction et qui est utilisé par un "programme" particulier appelé le noyau (kernel). Le second est le mode utilisateur (user mode ou user land), utilisé pour l'exécution de tous les autres programmes et qui active notamment des mécanismes d'isolation des processus (programmes en cours d'exécution), de protection de la mémoire et de l'accès aux périphériques.

Par exemple, sous Android, des programmes tels que le shell, les applications Java ou encore les scripts que vous écrivez sont exécutés en mode utilisateur. Le noyau d'Android est quant à lui un noyau Linux. Sa fonction est de gérer l'utilisation du processeur, la mémoire, les periphériques, le système de fichier, etc... Lorsqu'un programme utilisateur veut accéder à l'une de ces ressources (par exemple, lire un fichier), il doit nécessairement interagir avec le noyau qui se chargera d'exécuter la tâche pour son compte (par exemple, pilotage du disque dur pour récupérer les secteurs contenant les données). Le noyau fournit des appels systèmes pour réaliser cette abstraction (par exemple read, write, kill, etc.). Les appels systèmes, vus d'un programme, sont comme des appels de fonctions. Leur implémentation réelle diffère, notamment à cause du changement de mode qui doit s'ensuivre, du mode utilisateur vers le mode superviseur.

Nous vous proposons dans ce TD d'étudier plus en détails ce mécanisme en utilisant un module noyau écrit pour l'occasion. Linux permet en effet de modifier le noyau en cours d'exécution grâce à un système d'insertion/suppression de modules. Un module noyau est un programme écrit en C selon des conventions particulières, fournissant des fonctionnalités supplémentaires au noyau et pouvant être chargé "à la demande". Une fois chargé, le code du module est exécuté en mode noyau et a accès à toutes les fonctions préexistantes du noyau, ce qui lui permet en pratique d'accéder sans restriction à toutes les fonctionnalités de la machine. Sauf exception, le module peut être ultérieurement retiré du noyau lorsque ses fonctionnalités ne sont plus nécessaires.

La plupart des modules contiennent du code pour piloter un ou plusieurs périphérique(s) particulier(s) et ne seront donc chargés que sur une machine qui inclut de tels périphériques. Cela permet d'économiser de la mémoire noyau en évitant de charger du code inutile. Les modules sont aussi utilisés pour apporter des fonctionnalités de plus haut niveau, comme la possibilité d'interpréter un type supplémentaire de systèmes de fichiers, de spécifier la façon dont le trafic réseau ou vers les disques est traité (dans quel ordre les requêtes sont-elles prises en compte?) ou encore des implémentations d'algorithmes de cryptographie.

Pour simplifier, le module noyau que nous allons utiliser est d'un type un peu particulier : il s'agit d'un module réalisant un rootkit. Les rootkits, qui sont utilisés par des pirates, permettent, une fois installés sur une machine, d'en prendre le contrôle à distance, c'est-à-dire d'y accéder de manière "cachée" (difficile à détecter), de masquer toute opération réalisée au travers de cet accès et d'effacer ensuite les éventuelles traces laissées. Le nom rootkit provient de root, l'utilisateur administrateur de la machine qui a habituellement tous les droits, puisque les rootkits permettent généralement d'effectuer n'importe quelle opération (y compris lire impunément les fichiers des utilisateurs de la machine à leur insu!). Ils comportent parfois du code destiné à être exécuté en mode noyau, ce qui permet de manipuler (souvent dans le but de cacher) de manière plus fiable les informations fournies par le système aux programmes (fonctionnant, eux, en mode utilisateur et étant dépendants du noyau).

Les rootkits sont couramment utilisés par des pirates pour prendre le contrôle d'un ensemble de machines qui réaliseront diverses tâches pour leur compte, comme par exemple l'envoi de spam, le déclenchement d'attaques réseaux, ou plus simplement la récupération de données sensibles (tout type de documents mais également mots de passes, clés privées, etc...). Des rootkits ont même été utilisés par certaines grandes entreprises de l'industrie du disque pour pouvoir savoir quels étaient les fichiers MP3 lus par des clients de leurs CDs...

Un module de type rootkit est purement logiciel et ne nécessite pas qu'un "vrai" periphérique soit connecté à la machine (ici Android), ce qui a l'avantage de la simplicité. Le code source de notre rootkit est disponible dans le fichier android_rootkit.c. Vous pouvez le consulter pour plus de détails.

Le code est formé de différentes catégories de fonctions :

2. Utilisation du module rootkit

Lancez l'émulateur comme d'habitude (ligne de commande ou Eclipse, cf. TD 1).

Chargez le module à l'aide de la commande modprobe:

modprobe android_rootkit

Vérifiez que tout s'est bien passé grâce à la commande lsmod, qui donne la liste courante des modules chargés dans le noyaux.

Le noyau fournit également de nombreux messages d'information ou d'erreur. Vous pouvez consulter ces "logs" avec la commande dmesg.

Une fois notre module chargé, il faut, pour pouvoir l'utiliser, créer un fichier spécial de type caractère représentant un pseudo-périphérique qui permet de communiquer avec le code du module. Ce fichier permet de faire la passerelle entre les programmes en mode utilisateur et le module noyau : toute opération réalisée sur ce fichier par un programme (lecture, écriture, etc...) déclenche in fine l'appel à une fonction correspondante implémentée dans le module. Vous avez déjà rencontré une telle situation avec le périphérique spécial loopback dans le TD 5. Le module android_rootkit indique au noyau qu'il va utiliser un périphérique spécial identifié par le numéro majeur 127, grâce à la fonction register_chrdev appelée au chargement du module.

Créez le fichier spécial /dev/rootkit, de type caractère, avec le numéro majeur 127 et un numéro mineur 0, à l'aide de la commande mknod.

Le fichier /proc/rootkit est créé automatiquement au chargement du module; il permet d'activer ou de désactiver notre module.

Activez maintenant le module en écrivant 1 dans le fichier /proc/rootkit. Vous pourrez le désactiver à tout moment en y écrivant 0. La lecture du fichier vous donne simplement la valeur courante.

3. Utilisation du module

Notre rootkit permet essentiellement de :

  1. Cacher des fichiers dont le nom correspond à un motif paramétrable.
  2. Devenir root en détournant l'appel système kill.

3.1. Cacher des fichiers avec le rootkit

Créez un fichier dont vous choisirez le nom, puis cachez ce fichier grâce au rootkit. Pour ce faire, il suffit d'écrire le nom du fichier (ou une partie de ce nom) dans /dev/rootkit.

Utilisez cette methode pour cacher d'autres fichiers. Vous pouvez connaître le motif courant en lisant /dev/rootkit.

Si tout s'est bien passé, les fichiers ainsi cachés n'apparaissent plus lorsque l'on fait ls. Pour comprendre comment marche le module, nous allons utiliser un outil de débuggage : strace. Cet outil donne la liste des appels systèmes utilisés par un programme, avec leurs arguments.

Téléchargez le programme strace et copiez-le dans le répertoire /bin d'Android en utilisant la commande adb push.

Lancez strace /system/bin/ls. Attention, strace donne la liste de TOUS les appels système effectués, et la liste est longue. On peut utiliser

strace -o fichier /system/bin/ls
pour diriger la sortie dans un fichier (voir la page de manuel). Vous remarquerez, vers la fin de la liste, un appel à getdents. Cet appel système renvoie la liste des fichiers d'un répertoire.

Attention : Android fournit également un programme strace mais celui-ci est limité et ne fournit pas d'informations sur certains appels systèmes. Vous devez donc installer celui qui vous est fourni.

L'appel système getdents64 est utilisé par ls pour obtenir la liste des fichiers d'un répertoire. Plus précisément, ls utilise la fonction readdir, définie par le standard POSIX (standardisation des systèmes UNIX). L'implémentation de cette dernière utilise getdents64. En détournant cet appel système, notre rootkit fournit de fausses informations à ls, qui est "trompé".

3.2. Passer le module en mode debug

Il existe une autre manière d'interagir avec le noyau : l'utilisation de l'appel système ioctl sur un fichier spécial. Cet appel système sert à spécifier des options sur des fichiers "spéciaux", i.e., de type spécial, c'est-à-dire de type caractère ou bloc, ou bien un fichier de type régulier mais présenté par un système de fichier spécial, comme proc.

Pour notre module android_rootkit, nous allons utiliser ioctl sur le fichier /dev/rootkit. On peut utiliser cet appel système grâce à la commande du même nom ioctl :

ioctl device_file int
La fonction rootkit_ioctl dans android_rootkit.c est appelée lors d'une requête ioctl. La valeur entière 1 met le module en mode debug, tandis que la valeur 2 le fait sortir de ce mode.

A l'aide de la commande ioctl, passez en mode debug. Cachez des fichiers comme au paragraphe précédent. Affichez les logs du noyau en utilisant dmesg. Que constatez-vous?

3.3. Devenir root avec le rootkit

En plus de cacher des fichiers, le rootkit permet à n'importe qui de devenir root. Pour tester cette fonctionnalité, nous allons tout d'abord devenir un "simple" utilisateur. Android ne propose pas, par défaut, d'autre utilisateur que root.

Créez un nouvel utilisateur grâce aux étapes suivantes:

Note 1 : le répertoire /etc n'est pas persistant. Vous devrez reprendre cette question si vous redémarrez Android.

Note 2 : la méthode habituelle de création d'un nouvel utilisateur est l'utilisation de la commande adduser. Pour tester son fonctionnement, vous pouvez créer un autre utilisateur. Les autres commandes de gestion des utilisateurs et des groupes qu'il est utile de connaître sont deluser, addgroup et delgroup.

Utilisez la commande id pour connaître votre identité (root). Lancez ensuite un shell avec les droits de l'utilisateur normal créé précédemment, grâce à la commande su. Vérifiez que tout s'est bien passé en utilisant à nouveau id.

En tant qu'utilisateur normal, tapez:

ls /home

Que se passe-t-il?

En tant qu'utilisateurs, nos droits sont limités. Le rootkit nous permet de devenir root en appelant kill avec des arguments particuliers habituellement inutilisés avec cette commande.

Le rootkit remplace en effet l'appel système kill par sa propre implémentation, contenue dans la fonction hacked_kill dans android_rootkit.c. Cette fonction a le même comportement par défaut que kill sauf si l'identifiant du processus est 666 et le signal envoyé est 666. Dans ce cas particulier, le rootkit modifie les droits du processus courant pour qu'il semble avoir été lancé par root.

Utilisez la commande kill avec les arguments adéquats pour devenir root. Verifiez avec la commande id, puis en listant le contenu de /home, que la commande a bien fonctionné.

3.4. Cacher des processus

Bien que cela ne soit pas sa fonction principale, le rootkit permet, en l'état, de cacher des processus.

Utilisez la commande strace /system/bin/ps. Que peut-on en déduire sur le fonctionnement de la commande ps ?

Utilisez le rootkit pour cacher le processus com.android.alarmclock. Vérifier en utilisant à nouveau ps.

Désactivez finalement le rootkit en le déchargeant avec la commande modprobe.

4. Détection d'un rootkit

Adoptons maintenant le point de vue de l'administrateur système qui cherche à détecter un éventuel rootkit sur les machines qu'il administre.

4.1. Rootkit en mode utilisateur

Le rootkit présenté ici est un module noyau, mais de nombreux autres rootkits fonctionnent autrement : ils modifient directement les programmes présents sur le système, tels que ps ou ls pour cacher des processus ou des fichiers.

Expliquez pourquoi cette technique de modification des informations systèmes est, pour les exemples cités, moins puissante que l'utilisation d'un module noyau. Voyez-vous en particulier un moyen de la contourner qui ne fonctionnerait pas avec notre rootkit?

Une parade contre ces rootkits consiste à calculer des sommes de contrôle (cryptographic hashes) pour les programmes du système. Les propriétés des fonctions cryptographiques utilisées pour calculer ces sommes de controle garantissent que la probabilité pour que deux fichiers différents produisent la même somme de contrôle est très faible.

L'administrateur peut donc calculer régulièrement la somme de contrôle des programmes présents dans /bin. Si la somme de contrôle change, cela indique que le fichier a été modifié.

Utilisez la commande md5sum pour calculer les sommes de contrôle de ps et ls. Vérifiez que ces sommmes concordent avec celles de votre voisin(e).

Cette parade à base de somme de contrôle fonctionnerait-elle avec notre rootkit? Vérifiez-le.

Imaginons qu'un rootkit en mode utilisateur permette à un pirate de se connecter à la machine modifiée sur le port 666 (pour changer!).

Proposez une méthode de détection de cet ouverture utilisant la commande netstat. Proposez-en une autre à partir des commandes que vous connaissez. On supposera que toutes ces commandes ont conservé leur fonctionnement normal.

4.2. Rootkit en mode noyau

Une fois notre module chargé, l'administrateur du système peut tout de même le détecter facilement.

Proposez au moins 3 méthodes différentes de détection de notre rootkit.