Vous en aviez peut-être entendu parler : le 1er février 2008, Wojciech Purczynski a rapporté aux développeurs du kernel Linux une
vulnérabilité critique touchant un appel système, sys_vmsplice(). La vulnérabilité a été rendue publique le 8 février, soumise au bugtraq le 12 et a permis le piratage d'une multitude de
serveurs à travers le monde, malgré la rapidité de correction de la faille. Effectivement, la PoC (Proof Of Concept) largement diffusée permet de gagner les droits root sur n'importe quelle machine Linux Intel 32 bits. Bien sûr,
l'exploitation est possible sous d'autres architectures avec un programme adapté. Depuis 5 ans qu'elle traîne ici, elle est un peu
dépassée, mais permet d'appréhender les problématiques liées aux vulnérabilités du kernel.
I°) Le noyau linux : les bases
Un noyau monolithique
D'après Tanenbaum, l'architecture du noyau de Linux était censé condamner celui-ci à tomber aux oubliettes (cf. le
très célèbre mail Linux is obsolete).
Le premier point de son argumentation était le fait que le kernel soit monolithique. Qu'est ce qu'un kernel monolithique ? Voici la traduction
de son premier paragraphe qui l'explique parfaitement :
"La plupart des vieux OS sont monolithiques : c'est-à-dire que le système d'exploitation n'est qu'un seul fichier a.out qui tourne
en "Kernel Mode". Cet exécutable contient la gestion des processus, de la mémoire, du système de fichier et du reste. Des exemples de tels OS sont UNIX, MS-DOS,
VMS, MVS, OS/360, MULTICS et d'autres."
Autrement dit, chaque couche du noyau est intégrée dans un seul programme qui va tourner en Kernel Mode à la place du processus qui a la main sur le CPU.
L'alternative proposée par Tanenbaum est un noyau microlithique (utilisé par exemple dans MINIX, Apple MacOS X ou la série des noyaux Windows NT de Microsoft). Un tel noyau est
dit modulaire car ce qui compose le noyau est séparé en une multitude de modules tournant indépendamment et communiquant simplement comme n'importe quelle application multitâche.
Le micronoyau n'effectue que les opérations liées directement au hardware, les interruptions et la communication inter-process.
Malgré le fait que la recherche ne se base de nos jours presque que sur des noyaux microlithiques, il faut reconnaître que dans l'état actuel des choses, les noyaux monolithiques
sont souvent plus rapides, bien qu'ils gèrent un peu moins bien la mémoire (car dans les noyaux microlithiques, les fonctions non-nécessaires peuvent êtres déchargées, tandis
qu'un noyau monolithique est présent en totalité en permanence en mémoire centrale).
Ceci dit, Linux offre aussi une structure modulaire permettant l'ajout de fonctions du kernel (utilisé par exemple pour tout ce qui est pilotage du matériel, hors processeur). Cette
fonctionnalité va notamment nous permettre de réaliser notre correctif à chaud en ajoutant du code kernel.
Le Kernel Mode ou mode système
Les processeurs ont toujours au moins deux niveaux de fonctionnement différents qui ne disposent pas des mêmes droits d'accès aux différentes instructions
machine. Un code sous Linux peut donc tourner dans deux modes distincts : User Mode ou Kernel Mode. Quand un programme tourne en User Mode, il ne peut pas accéder
directement aux données du noyau ou des autres programmes : chaque programme tourne dans un contexte spécial, avec un espace d'adressage propre. Essayer d'accéder à un autre
espace d'adresses non rattaché au processus courant est impossible. Au contraire, quand un processus tourne en Kernel Mode, plus aucune restriction n'existe.
En pratique, le noyau n'est pas réellement un processus mais un gestionnaire de processus : il créé, élimine et synchronise les process existants (sous Linux, les fonctions
étant responsables de cet aspect sont rattachées au scheduler).
Chaque processus tournant en User Mode peut passer en Kernel Mode quand le contexte l'exige.
Il peut y avoir trois raisons :
- les appels systèmes (utilisation de fonctions fournies par le kernel, comme la communication inter-process, la gestion de signaux ou de fichiers, etc..)
- les interruptions
- la préemption (quand le process a epuisé son temps CPU, on effectue une commutation de contexte pour donner le CPU à un autre processus)
Quand le noyau a satisfait la demande du processus, il le ramène en User Mode.
Nous allons maintenant nous intéresser plus particulièrement aux routines du kernel appellées appels systèmes, ou syscalls qui représentent finalement l'interface
entre les couches basses de la machine et la couche logicielle.
Les appels système
Linux dispose d'un ensemble d'appels systèmes permettant différentes fonctions comme l'ouverture de fichiers, l'exécution de programmes, etc.. La liste
complète des appels systèmes avec leur numéro est disponibles dans le header du kernel /include/asm/unistd_32.h par exemple pour Intel x86. De plus, le fichier
/proc/kallsyms contient l'intégralité des symboles objets des appels systèmes avec leurs adresses mémoire. On remarque d'ailleurs au passage que toutes leurs adresses
sont plus grandes que 0xc0000000 : effectivement, les adresses entre 0x00000000 et 0xc0000000 sont dédiées aux process utilisateurs et constituent le segment utilisateur, alors que les adresses plus basses sont
réservées pour le noyau et ses segments de mémoire propres (Text, Data, Stack..).
Il y a sous Intel x86 deux façons de passer en Kernel Mode : l'instruction 0x80 que nous utilisons par exemple dans notre shellcode ou la nouvelle instruction sysenter
plus rapide et recommandée mais un peu plus compliquée à utiliser.
A l'initialisation, le noyau charge en mémoire la table des appels systèmes, ou SysCall Table dont on peut vérifier facilement l'existence dans la carte du système
qui contient tous les symboles de celui-ci :
$ cat /boot/System.map-`uname -r` | grep sys_call_table
c02c3700 R sys_call_table
Cette table contient exactement NR_syscalls entrées (défini dans asm/unistd.h). Chaque entrée est un pointeur vers le début du code de l'appel système correspondant.
Ainsi, la première entrée de la table correspond au syscall 0 (restart_syscall), donc pointe vers le code de l'appel système sys_restart_syscall.
Avec ces brefs rappels nous pouvons maintenant appréhender sans trop rentrer dans les détails mais sans trop rester superficiels non plus les détails de la faille vmsplice,
d'un des programmes d'exploitation et de notre correctif.