Dans sa version 2.1.0, le serveur FTP sécurisé vsftpd de Chris Evans a intégré un mécanisme de sandboxing, son auteur le décrivant simplement comme :
An ambitious new built-in sandbox. Think of it as privsep++, but more
on this in an upcoming post and paper.
De quoi nous rendre curieux ! Hop, on télécharge les sources de la
2.0.7 et la 2.1.0 et on fait un diff entre les deux répertoires : pas
grand chose, on peut juste observer qu'il y a des appels à
ptrace_sandbox_alloc() et ptrace_sandbox_run() et c'est tout : cela
signifierait donc que la sandbox est complètement transparent pour le
code de l'application, une bonne chose !
Introduction à la sandbox
On constate néanmoins l'ajout de deux fichiers : ptracesandbox.c et
ftppolicy.c. Les choses sérieuses commencent enfin...
D'après le nom de fichier, on peut se douter que la sandbox repose sur
ptrace() (aka "the unique and arcane syscall"). Effectivement, elle
utilise PTRACE_SYSCALL qui permet d'interrompre le processus traçé à
l'entrée dans un appel système (dans la routine syscall_trace_entry
et de passer la main au traceur. vsftpd implémente ainsi toute sa
logique dans la fonction ptrace_sandbox_handle_event() qui vérifie la
provenance du signal puis regarde quel appel système est demandé.
Vérification de l'appel système
Ce check n'est pas aussi simple que prévu, Chris Evans a ainsi trouvé
la vulnérabilité CESA-2009-001 dans le noyau Linux
le mois dernier permettant de contourner des mécanismes de protection
basés sur ptrace() (comme systrace).
L'astuce est que sur les architectures supportant à la fois le mode 32
et 64 bits, les numéros d'appel systèmes ne sont pas les mêmes suivant
le mode dans lequel on est (le syscall numéro 2 est open() en 32 bits,
mais correspond à fork() en 64 bits). Il faut alors prendre des
précautions particulières pour s'assurer que tout le monde parle la
même architecture :
Si on a intercepté un
int80ousysenter, on est sûr d'être en 32 bits.Si c'est par l'instruction
syscall, les dés sont lançés, il faut alors consulter le sélecteur CS afin de vérifier s'il référence une table dont on connait son "type" (32 ou 64 bits).
Une fois le numéro d'appel système (vraiment) connu, la sandbox va
consulter deux tables, toutes deux indexées par numéro d'appel
système. La première indique si le syscall est autorisé ou non, la
deuxième donne le callback associé à l'appel système (par exemple, il
y a un callback pour open() qui permet de vérifier l'utilisation de
O_RDONLY).
Création de processus
Toutes les méthodes de création de processus sont interdites dans la policy. Chaque déviation à la politique est fatale pour le processus qui est tué par un SIGKILL, cette brutalité est ici problématique comme nous allons le voir. En effet, lorsque le traceur est prévenu, il est en mesure de modifier le numéro d'appel système que voulait exécuter le processus afin de le rendre inoffensif. Le noyau exécute alors l'appel système, puis rend la main au processus en délivrant tous les signaux en attente.
Ici, le problème est que lorsque la sandbox voit que c'est un fork() qui veut être exécuté, elle tue le processus (en fait, elle ajoute un signal SIGKILL dans la liste des signaux en attente) mais le noyau continue de dérouler la routine syscall et donc d'exécuter la création du fils.
Néanmoins, lorsque le parent est schedulé, le SIGKILL est délivré et
il est effectivement tué. Mais le fils existe toujours! C'est pour
cela que la sandbox utilise PTRACE_SETOPTIONS avec
TRACE_{VFORK,FORK,CLONE} pour tracer par défaut tous les fils que
pourrait créer un processus et les tuer quand bon lui semble.
C'est là que je suis perplexe puisqu'on a vu qu'on pouvait dire au
noyau "Non en fait, le processus a voulu exécuter le syscall exit(),
pas fork() mais tue le quand même après". Et c'est effectivement ce
que fait la sandbox : avant de tuer le processus par trois moyens
différents, elle ré-écrit les registres afin de pointer sur exit() donc
tout est fait proprement et le SETOPTIONS précédent est ici
superflu. Vu que Chris est sûrement paranoïaque, dans le doute, il a
préfèré mettre ceinture et bretelles.
Protection du processus monitorant
On comprend bien que la sécurité de la sandbox repose uniquement sur
le processus monitorant. Si celui-ci tombe, tous les mécanismes de
sécurité sont compromit, c'est pour cette raison que lors de
l'initialisation d'un nouveau processus, chaque fils fait un
prctl(PR_SET_PDEATHSIG, SIGKILL, 0, 0, 0) afin que le noyau tue le
processus si son père (le moniteur) meurt.
Conclusion
Y a pas à dire, Chris Evans a réalisé ici un véritable chef-d'œuvre ! C'est la première fois que je vois une sandbox qui fait vraiment son rôle sans être vulnérable à la première attaque. Le design a été particulièrement travaillé, tout est clean et on sent qu'il y a eu de la réflexion derrière chaque fonction.
Impressionnant ! En plus, tout le moteur n'est contenu que dans un seul fichier C rendant son importation dans d'autres projets très simple.