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 :

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.