Introduction FAQ Glossary Project TD 1 TD 2 TD 3 TD 4 TD 5 TD 6 TD 7 TD 8
Retour au cours.
Objectifs :
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 :
module_init
et
module_exit
).
/dev/rootkit
et
/proc/rootkit
, ...).
hacked_kill
et
hacked_getdents64
).
get_sys_call_table
) : on recherche la table
contenant les addresses des fonctions pour tous les appels
systèmes et on remplace pour les appels qui nous intéressent
l'adresse de la fonction normale par l'addresse de nos
fonctions.
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.
Notre rootkit permet essentiellement de :
root
en détournant l'appel système kill
.
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/lspour 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é".
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 intLa 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?
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:
Ajoutez la ligne suivante au fichier
/etc/passwd
:
user:x:1000:1000:Linux User,,,:/home/user:/bin/sh
Consultez la page de manuel de passwd pour comprendre la signification de cette ligne.
Créez un nouveau groupe d'identifiant
1000
nommé users
, duquel
le nouvel utilisateur user
sera membre,
en ajoutant la ligne suivante au fichier
/etc/group
:
users:x:1000:
Créez un répertoire /home/user
et
utilisez chown
pour
l'attribuer à l'utilisateur user
et au
groupe users
.
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
.
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
.
Bien que cela ne soit pas sa fonction principale, le rootkit permet, en l'état, de cacher des processus.
Adoptons maintenant le point de vue de l'administrateur système qui cherche à détecter un éventuel rootkit sur les machines qu'il administre.
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é.
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.
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.